Imagine this scenario: in our app we have the following component structure:
-
Navbar component
- holds the
userstate and alogoutfunction (setUser(null)) - inside Navbar, we have NavLinks
- inside NavLinks, we have UserContainer
- holds the
Now, we want to pass user and logout to UserContainer.
If we pass them as props from Navbar → NavLinks → UserContainer, this becomes prop drilling. Prop drilling isn’t very clean, because we’re passing props through components that don’t actually need them, just to reach the target component.
This is where Context API, Redux, and other state-management libraries help.
Below is a simple example of how we can use Context API for this case.
Navbar
import { createContext, useState } from 'react';
import NavLinks from './NavLinks';
export const NavbarContext = createContext();
const Navbar = () => {
const [user, setUser] = useState({ name: 'Yu' });
const logout = () => setUser(null);
return (
<NavbarContext.Provider value={{ user, logout }}>
<div className="navbar">
<h5>ContextAPI</h5>
<NavLinks />
</div>
</NavbarContext.Provider>
);
};
export default Navbar;
NavLinks
This component simply renders its content, including UserContainer.
UserContainer
import { useContext } from 'react';
import { NavbarContext } from './Navbar';
const UserContainer = () => {
// const value = useContext(NavbarContext);
const { user, logout } = useContext(NavbarContext);
return (
<div className="user-container">
{user?.name ? (
<>
<p>{`Hello, ${user.name.toUpperCase()}!`}</p>
<button className="btn" onClick={logout}>
Logout
</button>
</>
) : (
<p>Please login</p>
)}
</div>
);
};
export default UserContainer;
Summary
There are just a few steps needed:
1- Create a context in the parent component
export const NavbarContext = createContext();
2- Wrap the component tree with NavbarContext.Provider and pass the shared data via value
This makes the data accessible to all nested components.
3- Consume the context in any child component
const { user, logout } = useContext(NavbarContext);
And that’s it — no prop drilling, cleaner code, and easier state sharing across components.
Credits: John Smilga’s course
Top comments (2)
💡 Extra tip: We can also wrap the Context logic inside a custom hook to make it cleaner and easier to reuse.
Instead of calling
useContext(NavbarContext)everywhere, we create a custom hook and use that instead.Navbar:
UserContainer:
This approach:
useContext(...)You can also move the context and the custom hook into a separate file (e.g.
NavbarContext.js) for better structure as the app grows.🌍 Global Context API – Clean & Scalable Example
A common and recommended approach is to define your global context in a dedicated folder and expose a custom hook for consuming it. This keeps your components clean and avoids repeating
useContextlogic everywhere.📁 Suggested Folder Structure (Convention)
📌
context/GlobalContext.jsx📌
main.jsxWrap your application with the global provider so all components can access the shared state.
📌
components/App.jsxAny child component can now access the global state using the custom hook.
✅ Why This Pattern Works Well
This is a solid baseline for global state using Context API, and you can easily extend it with more state, reducers, or side effects later.