Share state across React components without prop drilling using the useContext hook.
const ThemeContext = React.createContext();const UserContext = React.createContext({
user: null,
setUser: () => {}
});export const ThemeContext = React.createContext();// theme-context.js
import { createContext } from "react";
export const ThemeContext = createContext("light");interface User { id: number; name: string; }
interface UserContextType { user: User | null; setUser: (u: User) => void; }
const UserContext = createContext<UserContextType | undefined>(undefined);function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Main />
<Footer />
</ThemeContext.Provider>
);
}function Layout({ children }) {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}function UserProvider({ children }) {
const [user, setUser] = useState(null);
const value = useMemo(() => ({ user, setUser }), [user]);
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}function App() {
return (
<AuthProvider>
<ThemeProvider>
<Router>
<AppRoutes />
</Router>
</ThemeProvider>
</AuthProvider>
);
}function Header() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Current theme: {theme}
</button>
);
}function DeepComponent() {
const { user } = useContext(UserContext);
return <p>User: {user?.name}</p>;
}// Clear: names the exact values this component uses
const { theme } = useContext(ThemeContext);// This component re-renders every time theme or setTheme changes
const { theme, setTheme } = useContext(ThemeContext);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>;
}function useAuth() {
const { user } = useContext(AuthContext);
return {
user,
isLoggedIn: !!user,
isAdmin: user?.role === "admin"
};
}// users import useUser, not UserContext directly
export { useUser }; // export hook
// don't export UserContext — keep it private// Scope SidebarContext to the sidebar, not the whole app
function Sidebar() {
return (
<SidebarContext.Provider value={collapsed}>
<SidebarPanel />
</SidebarContext.Provider>
);
}// features/cart/cart-context.jsx — lives next to the feature, not in a top-level contexts/ folder
export const CartContext = createContext(null);// Fast-changing values fit local state or a state library better than context
const [query, setQuery] = useState(""); // keep local, not in context// Don't reach for Context just to avoid one prop
// Context is useful for truly global data: auth, theme, localetest("shows username from context", () => {
render(
<UserContext.Provider value={{ user: { name: "Alice" }, setUser: jest.fn() }}>
<UserDisplay />
</UserContext.Provider>
);
expect(screen.getByText("Alice")).toBeInTheDocument();
});function renderWithProviders(ui) {
return render(<AuthProvider><ThemeProvider>{ui}</ThemeProvider></AuthProvider>);
}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.