Search code examples
javascriptreactjscreate-react-app

React state/child update not behaving as I expected it to


Inside of my react application there are two components

Navbar

import React, { Component } from 'react';
import NavLink from './navlink';

class Navbar extends Component {
    state = {
        links: [
            {
                title: "Music",
                active: false
            },
            {
                title: "Home",
                active: false
            },
            {
                title: "Discord",
                active: false
            }
        ]
    }

    updateNavlinks = title => {
        const links = this.state.links
        for (const link in links){
            if (links[link].title != title){
                links[link].active=false;
            }
            else{
                links[link].active=true;
            }
        }
        console.log(links);
        this.setState({links})
    };

    render() { 
        return (
            <div id="Navbar">
                {this.state.links.map(link => <NavLink key={link.title} title={link.title} active={link.active} onClickFunc={this.updateNavlinks}/>) }
            </div>
        );
    }
}
 
export default Navbar;

Navlink

import React, { Component } from 'react';

class NavLink extends Component {
    state = {
        className: "navlink"+ (this.props.active?" active":"")
    }
    render() {
        return (
            <div className={this.state.className} onClick={() => this.props.onClickFunc(this.props.title)}>
                {this.props.title}
            </div>
        );
    }
}
 
export default NavLink;

My intention is to create a navbar where if the user selects a page, that <Navlink /> has its state changed. Once its state is changed (active=true), I want the classname to change, adding the "active" class and giving it the styles I want.

When updateNavlinks() is called, the state in <Navbar /> is changed, but it doesn't cause a visual change in the associated <Navlink />

Where did I go wrong with this? Is there a more simple way to accomplish this?


Solution

  • Here, you're mutating the existing state:

    updateNavlinks = title => {
        const links = this.state.links
        for (const link in links){
            if (links[link].title != title){
                links[link].active=false;
            }
            else{
                links[link].active=true;
            }
        }
        console.log(links);
        this.setState({links})
    };
    

    Never mutate state in React - that can make the script behave unpredictably. You need to call setState with a new object instead, so React knows to re-render:

    updateNavlinks = titleToMakeActive => {
        this.setState({
            links: this.state.links.map(
                ({ title, active }) => ({ title, active: title === titleToMakeActive })
            )
        });
    };
    

    Another problem is that you're assigning state in the constructor of the child component in NavLink:

    class NavLink extends Component {
        state = {
            className: "navlink"+ (this.props.active?" active":"")
        }
        render() {
            return (
                <div className={this.state.className} onClick={() => this.props.onClickFunc(this.props.title)}>
                    {this.props.title}
                </div>
            );
        }
    }
    

    This assigns to the state own-property when the component is mounted, but the component doesn't get un-mounted; the instance doesn't change, so state doesn't get assigned to again, even when the props change.

    To fix it, reference the props inside render instead of using state:

    class NavLink extends Component {
        render() {
            return (
                <div className={"navlink"+ (this.props.active?" active":"")} onClick={() => this.props.onClickFunc(this.props.title)}>
                    {this.props.title}
                </div>
            );
        }
    }