4

I am trying to make a simple NavBar with React.js. The problem I found myself in is the looping over all nav buttons and remove the "active" className and then add "active" to just that one clicked button.

I managed to make a state that toggles "active" to true on the clicked element which then in the className attribute does this If statement:

className={this.state.active ? "nav-item nav-link active" : "nav-item nav-link"}

Here is the full code:

import React, { Component } from 'react';

class NavButton extends Component {
    state = {
        active: false
    }

    setActive = () => {
        this.setState({
            active: !this.state.active
        })
    }

    render() {
        return (
            <a 
            className={this.state.active ? "nav-item nav-link active" : "nav-item nav-link"} 
            href={this.props.href}
            onClick={this.setActive}> {this.props.title} 
            </a>
        )
    }
}

class NavBar extends Component {

    buttons = [
        {
            title: "Home",
            key: 0
        },
        {
            title: "Team",
            key: 1
        },
        {
            title: "Discord",
            key: 2
        },
        {
            title: "Gallery",
            key: 3
        },
        {
            title: "Download",
            key: 4
        }
    ]

    render() {
        return (
            <nav className="navbar" id="navbarMain">
                <div></div>
                <div className="navbar-nav flex-row">
                    {this.buttons.map(button => <NavButton title={button.title} key={button.key} />)}
                </div>
                <div></div>
            </nav>
        )
    }
}

export default NavBar

This works, for just one element (don't mind that the active state goes false when it's true. The problem is, how would I do it in the React way to remove the active className in all other buttons?

With plain JS i have no issues to do that, i just loop over all elements that have the className "navbar-item" and set their classnames to be without the "active" one then add " active" to the pressed element like in this example https://www.w3schools.com/howto/howto_js_tabs.asp

Would you guys be able to help and tell me what would be the best react way to do this?

Much appreciated!

Matt Chowski
  • 111
  • 1
  • 2
  • 9

2 Answers2

3

A common pattern for these use-cases is to keep the relevant state in the parent, so that it is the parent (NavBar) that keeps track of which child (NavButton) is "active". The NavButton can then become a stateless component which takes "active" as a prop.

const NavButton = ({active, title, href, onSetActive}) => {
        return (
            <button 
            className={active ? "nav-item nav-link active" : "nav-item nav-link"} 
            href={href}
            onClick={onSetActive} > 
              {title} 
            </button>
        )
}

class NavBar extends React.Component {
  constructor(props) {
     super(props);
     this.state = {
       activeIndex: 0, // keep the active index in state
       buttons: [
        {
            title: "Home",
            key: 0
        },
        {
            title: "Team",
            key: 1
        },
        {
            title: "Discord",
            key: 2
        },
        {
            title: "Gallery",
            key: 3
        },
        {
            title: "Download",
            key: 4
        }
    ]
      }
    }
    
    handleChangeActive(newActiveIndex) {
     this.setState({activeIndex: newActiveIndex});
    }

    render() {
      const {activeIndex} = this.state;
        return (
            <nav className="navbar" id="navbarMain">
                <div></div>
                <div className="navbar-nav flex-row">
                    {this.state.buttons.map((button, buttonIndex) => 
                     /* determine which nav button is active depending on the activeIndex state */
                     <NavButton onSetActive={ () => this.handleChangeActive(buttonIndex)} active={buttonIndex === activeIndex } title={button.title} key={button.key} />)}
                </div>
                <div></div>
            </nav>
        )
    }
}

ReactDOM.render(<NavBar />, document.querySelector("#app"));
body {
  background: #20262E;
  padding: 20px;
  font-family: Helvetica;
}

#app {
  background: #fff;
  border-radius: 4px;
  padding: 20px;
  transition: all 0.2s;
}

li {
  margin: 8px 0;
}

h2 {
  font-weight: bold;
  margin-bottom: 15px;
}

.done {
  color: rgba(0, 0, 0, 0.3);
  text-decoration: line-through;
}

input {
  margin-right: 5px;
}
.nav-item.nav-link {
  background: grey;
}
.nav-item.nav-link.active {
  background: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<body>

<div id="app"></div>
</body>
jonahe
  • 4,820
  • 1
  • 15
  • 19
  • 1
    wow thanks a lot man! Great explanation and the code is clear and works perfectly! Marked as the correct answer – Matt Chowski Jun 23 '19 at 12:40
  • Sorry for bothering again, althought I am failing to see why other buttons get turn off? Why does activeIndex return false for the other unclicked elements if we set up activeIndex to be whatever the index of the clicked button is? – Matt Chowski Jun 23 '19 at 13:10
  • When you loop through the list of buttons, they each have different indexes (`buttonIndex`). The first will have index 0, then index 1 etc. And only one of them will be equal to the one you saved in as `activeIndex`. When you change the state `activeIndex` , another `buttonIndex` will be the one matching `activeIndex`. Hope that makes sense. – jonahe Jun 23 '19 at 13:14
  • The function you use in `.map` (or `.forEach`) receives the current item as the first argument, and the current index as the second argument. (There's also a thrird argument passed, with the whole array). Try pasting `["a","b","c"].forEach(console.log)` in browser console. For each item it will log 1: the item (letter), 2: the index, 3: the array. I hope I'm not stating the obvious. Maybe I misunderstood your question. – jonahe Jun 23 '19 at 13:23
1

I would move your state and logic to the NavBar component. It would be responsible to store and set the current active button, and pass it as prop to all buttons.

class NavBar extends Component {
  state = {
    activeButtonIndex: null;
  }

  buttons = [
    {
      title: "Home",
      key: 0
    },
    {
      title: "Team",
      key: 1
    },
  ];

  renderButton = (button, index) => (
    <NavButton 
      {...button} 
      isActive={this.state.activeButtonIndex === index}
      setActive={() => this.setState({ activeButtonIndex: index })}
    />
  );

  render() {
    return (
      <nav className="navbar" id="navbarMain">
        <div className="navbar-nav flex-row">
          {this.buttons.map((button, index) => this.renderButton(button, index)}
        </div>
      </nav>
    );
  }
}

const NavButton = ({ isActive, setActive, href, title }) => (
  <a 
    className={isActive ? "nav-item nav-link active" : "nav-item nav-link"} 
    href={href}
    onClick={setActive}
  >
    {title} 
  </a>
);
adesurirey
  • 2,549
  • 2
  • 16
  • 36