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>
);
}
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: