useEffect hook fix dependency array when a route is changed

Viewed 1240

I'm facing a problem with the functional component. I'm using a react-router-dom package for handling the routes. What I have is a list that contains links and buttons and when we click on the button a dropdown is opened. This dropdown also contains links.

  • Whenever user will change the route all dropdowns will close.
  • Whenever new dropdown is opened all others will also close.

How I'm manging the state of these dropdowns?

I'm treating the state of every dropdown as a separate object and onClick listener will pass a key / property as an argument that has a boolean value. Based on that value a class will be added on the dropdown.

I already achieve this with the help of a class component.

Class component:

import React, { Component } from "react";
import { Link, withRouter } from "react-router-dom";

class ToggleClass extends Component {
    state = {};

    toggle = dropdownName => {
        // If it has the true value then turn into false, It is a behavior of a toggle.
        if (this.state[dropdownName]) {
            this.setState({ [dropdownName]: false });
        }
        // If there is not any dropdown is opened it means state object is empty
        // Object.keys will return an empty array then please open that dropdown.
        else if (Object.keys(this.state).length === 0) {
            this.setState({ [dropdownName]: true });
        }
        // Else block will ony trigger when the user clicked on other dropdown,
        // then close the last dropdown and just open the new one
        else {
            Object.keys(this.state).forEach(i => {
                this.setState({ [i]: false });
            });
            this.setState({ [dropdownName]: true });
        }
    };

    onRouteChanged = () => {
        // Take a state object and convert all its keys into an array
        // and set it to false whenever a route changes.
        Object.keys(this.state).forEach(i => {
            // pageMenu: false
            this.setState({ [i]: false });
        });
    };

    componentDidUpdate(prevProps, prevState, snapshot) {
        if (this.props.location !== prevProps.location) {
            this.onRouteChanged();
        }
    }

    componentDidMount() {
        this.onRouteChanged();
    }

    render() {
        return (
            <ul className="list">
                <li>
                    <Link to="/link1">Link 1</Link>
                </li>

                <li>
                    <button onClick={() => this.toggle("dropdown1")}>Dropdown 1</button>

                    <ul className={this.state["dropdown1"] ? "open" : "hide"}>
                        <li>
                            <Link to="/link2">Link 2</Link>
                        </li>
                        <li>
                            <Link to="/link3">Link 3</Link>
                        </li>
                    </ul>
                </li>

                <li>
                    <button onClick={() => this.toggle("dropdown2")}>Dropdown 2</button>

                    <ul className={this.state["dropdown2"] ? "open" : "hide"}>
                        <li>
                            <Link to="/link4">Link 4</Link>
                        </li>
                        <li>
                            <Link to="/link5">Link 5</Link>
                        </li>
                    </ul>
                </li>
            </ul>
        );
    }
}

export default withRouter(ToggleClass);

But the problem is on the functional component by using hooks. react-router-dom has a hook named useLocation which returns the current pathname. But onRouteChanged function is updating the state. I cannot use this function inside useEffect hook it turns into an infinite loop.

useEffect(() => {
  onRouteChanged();
}, [location, onRouteChanged]);

But If I remove the onRouteChanged from the dependency array then linter will warn about this

React Hook useEffect has a missing dependency: 'onRouteChanged'. Either include it or remove the dependency array react-hooks/exhaustive-deps

useEffect(() => {
  onRouteChanged();
}, [location]);

Functional Component:

import React, { useEffect, useState } from "react";
import { Link, useLocation } from "react-router-dom";

const ToggleFunc = () => {
  const location = useLocation();
  const [state, setState] = useState({});

  const onRouteChanged = () => {
    Object.keys(state).forEach(i => {
      setState({ [i]: false });
    });
  };

  const toggle = dropdownName => {
    if (state[dropdownName]) {
      setState({ [dropdownName]: false });
    } else if (Object.keys(state).length === 0) {
      setState({ [dropdownName]: true });
    } else {
      Object.keys(state).forEach(i => {
        setState({ [i]: false });
      });
      setState({ [dropdownName]: true });
    }
  };

  useEffect(() => {
    onRouteChanged();
  }, [location]);

  return (
    <ul className="list">
      <li>
        <Link to="/link1">Link 1</Link>
      </li>

      <li>
        <button onClick={() => toggle("dropdown1")}>Box 1</button>

        <ul className={state["dropdown1"] ? "open" : "hide"}>
          <li>
            <Link to="/link2">Link 2</Link>
          </li>
          <li>
            <Link to="/link3">Link 3</Link>
          </li>
        </ul>
      </li>

      <li>
        <button onClick={() => toggle("dropdown2")}>Box 2</button>

        <ul className={state["dropdown2"] ? "open" : "hide"}>
          <li>
            <Link to="/link4">Link 4</Link>
          </li>
          <li>
            <Link to="/link5">Link 5</Link>
          </li>
        </ul>
      </li>
    </ul>
  );
};

export default ToggleFunc;

Can you guide me What is the optimal way to fix these errors?

Full example Codesandbox

1 Answers

I think you have to refactor the structure of the state and the toggle implementation, here is the fixed sandbox example (I didn't changed the state structure just fixed your issue).

https://codesandbox.io/s/solo98989-q11j1

Related