Search code examples
reactjsreact-routernavigationreactjs.net

Why is react-single-page-navigation only working in Components without constructor and state?


I'm currently learning react and I found this cool package: https://www.npmjs.com/package/react-single-page-navigation.

I started using it and everything went well. Then I wanted to make my web app responsive and so I built a mobile navigation (simple dropdown) for small screens.

First, this navigation worked too! The dropdown was always shown and i could klick on the buttons with their references and i got navigated to the wished section through a smooth scrolling. Because of the show-and-hide functionality of the dropdown, I added an constructor with the state menuVisible to the mobile navigation Component and two functions to handle the click on the "open dropdown" button and the click anywhere else but the dropdown items (standard dropdown mechanism).

This mechanism works well and the dropdwon looks good. But: The navigation does not work anymore. Instead, i always get "navigated" some pixels up and down on the first section of my web app.

Here's the working desktop navigation Component:

import React, {Component} from 'react';

export default class DesktopNavigation extends Component {
    render() {
        return (
            <nav className={this.props.sticky ? 'navbar navbar--sticky' : 'navbar'} id='mainNav'>
                <div className='navbar--logo-holder'>
                    <img alt='logo' className='navbar--logo'/>
                    <h1 className={this.props.sticky ? 'navbar--logo-text-sticky' : 'navbar--logo-text'}>Booyar</h1>
                </div>
                <ul className='navbar--link'>
                    <div ref={this.props.refs.Home} onClick={() => this.props.goTo('Home')}>
                        <div className={(this.props.activeElement === 'Home' ? 'active-page ' : '') + (this.props.sticky ? 'navbar--link-item-sticky' : 'navbar--link-item')}>
                            Home
                        </div>
                    </div>
                    <div ref={this.props.refs.About} onClick={() => this.props.goTo('About')}>
                        <div className={(this.props.activeElement === 'About' ? 'active-page ' : '') + (this.props.sticky ? 'navbar--link-item-sticky' : 'navbar--link-item')}>
                            About
                        </div>
                    </div>
                    <div ref={this.props.refs.Services} onClick={() => this.props.goTo('Services')}>
                        <div className={(this.props.activeElement === 'Services' ? 'active-page ' : '') + (this.props.sticky ? 'navbar--link-item-sticky' : 'navbar--link-item')}>
                            Services
                        </div>
                    </div>
                    <div ref={this.props.refs.Projects} onClick={() => this.props.goTo('Projects')}>
                        <div className={(this.props.activeElement === 'Projects' ? 'active-page ' : '') + (this.props.sticky ? 'navbar--link-item-sticky' : 'navbar--link-item')}>
                            Projects
                        </div>
                    </div>
                    <div ref={this.props.refs.Contact} onClick={() => this.props.goTo('Contact')}>
                        <div className={(this.props.activeElement === 'Contact' ? 'active-page ' : '') + (this.props.sticky ? 'navbar--link-item-sticky' : 'navbar--link-item')}>
                            Contact
                        </div>
                    </div>
                </ul>
            </nav>
        );
    }
}

And here are the not correctly working mobile navigation Components:

import React, {Component} from 'react';
import DropdownMenu from "./dropdownMenu";


export default class MobileNavigation extends Component {

    constructor(props) {
        super(props);
        this.state = {
            sticky: this.props.sticky
        }
    }

    callbackIsSticky = (isSticky) => {
        this.setState({sticky: isSticky})
    }

    render() {
        return (
            <nav className={this.state.sticky || this.props.sticky ? "mobile-navbar mobile-navbar-sticky" : "mobile-navbar"} id="mainNav">
                <div className="mobile-navbar--logo-holder">
                    <img alt="logo" className="mobile-navbar--logo"/>
                </div>
                <DropdownMenu
                    refs={this.props.refs}
                    activeElement={this.props.activeElement}
                    goTo={this.props.goTo}
                    sticky={this.props.sticky}
                    callback={this.callbackIsSticky}
                />
            </nav>
        );
    }
}
import React, {Component} from 'react';

/**
 * This class represents the dropdown menu in mobile view.
 */
export default class DropdownMenu extends Component {

    constructor(props) {
        super(props);
        this.state = {
            menuVisible: false
        }
        this.handleClick = this.handleClick.bind(this);
        this.handleOutsideClick = this.handleOutsideClick.bind(this);
    }

    handleClick() {
        if (!this.state.menuVisible) {
            document.addEventListener('click', this.handleOutsideClick, false);
            this.props.callback(true);
        } else {
            document.removeEventListener('click', this.handleOutsideClick, false);
            this.props.callback(false);
        }

        this.setState(stateBefore => ({
            menuVisible: !stateBefore.menuVisible,
        }));
    }

    handleOutsideClick(event) {
        if (this.node.contains(event.target)) {
            return;
        }

        this.handleClick();
    }

    render() {
        const items =
            <div className={'dropdown-items'}>
                <button
                    ref={this.props.refs.Home} onClick={() => this.props.goTo('Home')}
                    className={(this.props.activeElement === 'Home' ? "active-mobile-page " : "")}>
                    Home
                </button>
                <button
                    ref={this.props.refs.About} onClick={() => this.props.goTo('About')}
                    className={(this.props.activeElement === 'About' ? "active-mobile-page " : "")}>
                    About
                </button>
                <button
                    ref={this.props.refs.Services} onClick={() => this.props.goTo('Services')}
                    className={(this.props.activeElement === 'Services' ? "active-mobile-page " : "")}>
                    Services
                </button>
                <button
                    ref={this.props.refs.Projects} onClick={() => this.props.goTo('Projects')}
                    className={(this.props.activeElement === 'Projects' ? "active-mobile-page " : "")}>
                    Projects
                </button>
                <button
                    ref={this.props.refs.Contact} onClick={() => this.props.goTo('Contact')}
                    className={(this.props.activeElement === 'Contact' ? "active-mobile-page " : "")}>
                    Contact
                </button>
            </div>;

        return (
            <div className={'dropdown-menu' + (this.state.menuVisible ? ' dropdown-menu-open' : '')}
                 ref={node => this.node = node}>
                <button className={'hamburger hamburger--elastic' + (this.state.menuVisible ? ' is-active' : '')} onClick={this.handleClick} type={'button'}>
                    <span className="hamburger-box">
                        <span className={'hamburger-inner' + (this.state.menuVisible || this.props.sticky ? ' hamburger-inner-sticky' : '')}>
                        </span>
                    </span>
                </button>
                {this.state.menuVisible ? items : undefined}
            </div>
        );
    }
}

Of course, everything is wrapped by <ScrollNavigation elements={{Home: {}, About: {}, Services: {}, Projects: {}, Contact: {}}}>...</ScrollNavigation> as described in the documentation of react-single-page-navigation.

I decide whether to show mobile or desktop navigation with css and the following class:

import React, {Component} from 'react';
import MobileNavigation from "./mobileNavigation";
import DesktopNavigation from "./desktopNavigation";

export default class Navigation extends Component {

    render() {
        return (
            <div>
                <MobileNavigation
                    refs={this.props.refs}
                    activeElement={this.props.activeElement}
                    goTo={this.props.goTo}
                    sticky={this.props.isSticky}/>
                <DesktopNavigation
                    refs={this.props.refs}
                    activeElement={this.props.activeElement}
                    goTo={this.props.goTo}
                    sticky={this.props.isSticky}/>
            </div>
        );
    }
}

I hope this is enough information. I do not like to post my whole project, but i think this is not necessary. The desktop navigation works as expected. The mobile navigation just navigates millimeters in the Home section. I think there is a basic react "thing" causing that behaviour that I can't figure out, because the mobile nav works when i uncomment everything but the render methods (and the stuff in the render methods that calls the uncommented stuff ;) ).

If you need more information or have further questions, please ask! :)

Thanks in advance and happy holidays!

Maximotus


Solution

  • Ok, so my mistake was to put the reference of the scroll navigation ref={this.props.refs.Home} (etc.) to the buttons. I somehow misunderstood the instructions in the docs of react-single-page-navigation. The ref prop is like the 'anchor' so the scroll navigation knows where to scroll. And I added this ref to the buttons itself, so the navigation just scrolled to the positions of the buttons.

    So I just removed it from the buttons and everything works well now (same for desktop navigation, idk why it worked there before with the same mistake).

    <ul className='navbar--link'>
                        <div onClick={() => this.props.goTo('Home')}>
                            <div
                                className={(this.props.activeElement === 'Home' ? 'active-page ' : '') + (this.props.sticky ? 'navbar--link-item-sticky' : 'navbar--link-item')}>
                                Home
                            </div>
                        </div>
                        <div onClick={() => this.props.goTo('About')}>
                            <div
                                className={(this.props.activeElement === 'About' ? 'active-page ' : '') + (this.props.sticky ? 'navbar--link-item-sticky' : 'navbar--link-item')}>
                                About
                            </div>
                        </div>
                        <div onClick={() => this.props.goTo('Services')}>
                            <div
                                className={(this.props.activeElement === 'Services' ? 'active-page ' : '') + (this.props.sticky ? 'navbar--link-item-sticky' : 'navbar--link-item')}>
                                Services
                            </div>
                        </div>
                        <div onClick={() => this.props.goTo('Projects')}>
                            <div
                                className={(this.props.activeElement === 'Projects' ? 'active-page ' : '') + (this.props.sticky ? 'navbar--link-item-sticky' : 'navbar--link-item')}>
                                Projects
                            </div>
                        </div>
                        <div onClick={() => this.props.goTo('Contact')}>
                            <div
                                className={(this.props.activeElement === 'Contact' ? 'active-page ' : '') + (this.props.sticky ? 'navbar--link-item-sticky' : 'navbar--link-item')}>
                                Contact
                            </div>
                        </div>
                    </ul>
    

    Now, the triggers for the scroll navigation only define the goTo prop and the ref prop is defined by the sections of my website.