Search code examples
javascriptreactjsreact-propsreact-state

Change state of const in component from another component


I have a react app where I built a simple menu that changes state on click to open and close it. This works fine with the menu button that is in the same component. However I now want to be able to open the menu from another component, in this case from the start page hero.

I have two components: header.js and hero.js

What I'm trying to do is to open the menu when I press the button on the hero.

I tried to import the component, the action, and the state but nothing works. I know I'm doing it wrong but I don't know how to solve it and could not find any solution to it though I'm sure there is one.

Here is my code example.

header.js

import React, { useState } from 'react';

export default function Header() {
  const [isOpen, setIsOpen] = useState(false);

  const menuToggle = event => {
    setIsOpen(current => !current);
  };

  return (
    <header className={`header ${isOpen ? "open" : "closed"}`}>
      <button onClick={menuToggle}>Toggle menu</button>
    </header>
  );
}

hero.js

import setIsOpen from '../header/Header';

export default function Hero() {
  const menuToggle = event => {
    setIsOpen(true);
  };

  return (
    <section className="hero">
      <button onClick={menuToggle}>Toggle menu</button>
    </section>
  );
}

Solution

  • The solution is to lift the isOpen state up to a common ancestor component and pass the state and updaters/callbacks down as props to the appropriate components.

    Basic Trivial Example:

    Header

    export default function Header({ isOpen, menuToggle }) {
      return (
        <header className={["header", isOpen ? "open" : "closed"].join(" ")}>
          <button onClick={menuToggle}>Toggle menu</button>
        </header>
      );
    }
    

    Hero

    export default function Hero({ menuToggle }) {
      return (
        <section className="hero">
          <button onClick={menuToggle}>Toggle menu</button>
        </section>
      );
    }
    
    const ParentComponent = () => {
      const [isOpen, setIsOpen] = React.useState(false);
    
      const menuToggle = event => {
        setIsOpen(open => !open);
      };
    
      return (
        <>
          <Header isOpen={isOpen} menuToggle={menuToggle} />
          <Hero menuToggle={menuToggle} />
        </>
      );
    };
    

    If there isn't a direct parent-child relationship between these components then you can either pass the props through all intermediate components until you get to both Header and Hero, or you can implement a React Context to provide the value and avoid the props drilling issue.

    Example:

    export const MenuContext = React.createContext({
      isOpen: false,
      menuToggle: () => {},
    });
    
    export const useMenuContext = () => React.useContext(MenuContext);
    
    const MenuContextProvider = ({ children }) => {
      const [isOpen, setIsOpen] = React.useState(false);
    
      const menuToggle = event => {
        setIsOpen(open => !open);
      };
    
      return (
        <MenuContext.Provider value={{ isOpen, menuToggle }}>
          {children}
        </MenuContext.Provider>
      );
    };
    
    export default MenuContextProvider;
    

    Render MenuContextProvider as the common ancestor component, and downstream use the useMenuContext to access the context value, wherever the components are.

    App

    const App = () => {
      ...
    
      return (
        <MenuContextProvider>
          ... 
          ... Header somewhere down the ReactTree ...
          ... Hero somewhere down the ReactTree ...
          ...
        </MenuContextProvider>
      );
    };
    

    Header

    export default function Header() {
      const { isOpen, menuToggle } = useMenuContext();
    
      return (
        <header className={["header", isOpen ? "open" : "closed"].join(" ")}>
          <button onClick={menuToggle}>Toggle menu</button>
        </header>
      );
    }
    

    Hero

    export default function Hero() {
      const { menuToggle } = useMenuContext();
    
      return (
        <section className="hero">
          <button onClick={menuToggle}>Toggle menu</button>
        </section>
      );
    }
    

    For more details see: