React useContext Hook
Share state across React components without prop drilling using the useContext hook.
TL;DR
- 01Create a Context with createContext for shared data.
- 02Wrap components with a Provider to make data available.
- 03Use useContext to access data in any child component.
Tips
- 01Create custom hooks for contexts to simplify usage and add error checking for consumers.
Warnings
- 01Every context change causes all consumers to re-render — split contexts by concern to avoid performance issues.
Creating Context
- Create a context to hold shared data.
const ThemeContext = React.createContext(); - Provide an initial value as a fallback when no provider wraps the consumer.
const UserContext = React.createContext({ user: null, setUser: () => {} }); - Export the context for use in other components.
export const ThemeContext = React.createContext(); - Keep context creation in its own file for clarity.
// theme-context.js import { createContext } from "react"; export const ThemeContext = createContext("light"); - Use TypeScript generics to type the context value.
interface User { id: number; name: string; } interface UserContextType { user: User | null; setUser: (u: User) => void; } const UserContext = createContext<UserContextType | undefined>(undefined);
Creating a Provider
- Wrap your app with a Provider component.
function App() { const [theme, setTheme] = useState("light"); return ( <ThemeContext.Provider value={{ theme, setTheme }}> <Header /> <Main /> <Footer /> </ThemeContext.Provider> ); } - Provider passes value to all child components.
function Layout({ children }) { const [user, setUser] = useState(null); return ( <UserContext.Provider value={{ user, setUser }}> {children} </UserContext.Provider> ); } - Extract the provider into its own component to keep App clean.
export function ThemeProvider({ children }) { const [theme, setTheme] = useState("light"); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ); } - Memoize the context value to avoid re-rendering every consumer.
function UserProvider({ children }) { const [user, setUser] = useState(null); const value = useMemo(() => ({ user, setUser }), [user]); return ( <UserContext.Provider value={value}> {children} </UserContext.Provider> ); } - Nest multiple providers at the top of the tree.
function App() { return ( <AuthProvider> <ThemeProvider> <Router> <AppRoutes /> </Router> </ThemeProvider> </AuthProvider> ); }
Consuming Context
- Access context with useContext hook.
function Header() { const { theme, setTheme } = useContext(ThemeContext); return ( <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}> Current theme: {theme} </button> ); } - useContext works in any descendant component, no matter how deep.
function DeepComponent() { const { user } = useContext(UserContext); return <p>User: {user?.name}</p>; } - Returns the createContext default value if no matching Provider is above it, or undefined if no default was set.
- Destructure only the values you need to be explicit.
// Clear: names the exact values this component uses const { theme } = useContext(ThemeContext); - Re-renders happen whenever the context value changes.
// This component re-renders every time theme or setTheme changes const { theme, setTheme } = useContext(ThemeContext);
Custom Hooks
- Create a custom hook to simplify context access.
function useTheme() { const context = useContext(ThemeContext); if (!context) { throw new Error("useTheme must be inside ThemeProvider"); } return context; } function Header() { const { theme, setTheme } = useTheme(); return <button onClick={() => setTheme("dark")}>{theme}</button>; } - Custom hooks provide error checking and a cleaner API.
- Makes consuming context easier for other developers.
- Add computed values or helpers inside the custom hook.
function useAuth() { const { user } = useContext(AuthContext); return { user, isLoggedIn: !!user, isAdmin: user?.role === "admin" }; } - Export only the custom hook, not the raw context.
// users import useUser, not UserContext directly export { useUser }; // export hook // don't export UserContext — keep it private
Best Practices
- Place the Provider as close as possible to the components that need it, not always at the app root.
// Scope SidebarContext to the sidebar, not the whole app function Sidebar() { return ( <SidebarContext.Provider value={collapsed}> <SidebarPanel /> </SidebarContext.Provider> ); } - Colocate context with the feature that owns it instead of lifting everything to a shared global file — move it up only when a second, unrelated feature genuinely needs the same data.
// features/cart/cart-context.jsx — lives next to the feature, not in a top-level contexts/ folder export const CartContext = createContext(null); - Avoid context for state that changes on every keystroke or frame, like form input or animation values — high-frequency updates re-render every consumer and are a poor fit for the context model.
// Fast-changing values fit local state or a state library better than context const [query, setQuery] = useState(""); // keep local, not in context - Prefer local state or props for data used by only 1–2 components.
// Don't reach for Context just to avoid one prop // Context is useful for truly global data: auth, theme, locale - Test context consumers by wrapping them in the provider, not by mocking the context module.
test("shows username from context", () => { render( <UserContext.Provider value={{ user: { name: "Alice" }, setUser: jest.fn() }}> <UserDisplay /> </UserContext.Provider> ); expect(screen.getByText("Alice")).toBeInTheDocument(); }); - Build a small reusable test helper that wraps render with your real providers, so every test exercises the same provider tree your app uses in production.
function renderWithProviders(ui) { return render(<AuthProvider><ThemeProvider>{ui}</ThemeProvider></AuthProvider>); }
FAQ
Use useContext when data needs to be accessed by many components at different nesting levels, such as theme, locale, or auth state. Prop drilling becomes a maintenance burden once you're passing props through more than 2-3 layers of components that don't use the data themselves.
All consumers re-render whenever any value in the context object changes. Split your context into smaller, focused contexts by concern (e.g., separate AuthContext from ThemeContext) so that a change in one doesn't trigger re-renders in unrelated consumers.
Include an updater function in your context value alongside the data — pass a useState setter or a dispatch function from useReducer. Consumers can then call that function to trigger state changes that propagate back through the Provider.
useContext is built-in and ideal for low-frequency updates like theme or auth, with no extra dependencies. Redux adds middleware, devtools, and a strict unidirectional flow suited to complex, high-frequency global state — useContext alone can become hard to scale when many unrelated pieces of state share a single context.
In your custom hook, check that the returned context value is not undefined and throw a descriptive error if it is — for example: if (!ctx) throw new Error('useAuth must be used within an AuthProvider'). This gives a clear error instead of a silent undefined crash deep in the component tree.