Context cheatsheet.
Basic
const ThemeContext = createContext<"light" | "dark">("light");
function App() {
return (
<ThemeContext.Provider value="dark">
<Child />
</ThemeContext.Provider>
);
}
function Child() {
const theme = useContext(ThemeContext);
return <p>{theme}</p>;
}
React 19: shorthand provider
<ThemeContext value="dark">
<Child />
</ThemeContext>
.Provider is now optional in React 19.
With null default + custom hook
const AuthContext = createContext<AuthState | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState<AuthState>({ user: null });
const value = useMemo(() => ({ state, setState }), [state]);
return <AuthContext value={value}>{children}</AuthContext>;
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth outside provider");
return ctx;
}
Custom hook + null default = enforced provider boundary, friendly errors.
Multiple contexts (split)
const StateContext = createContext<State | null>(null);
const DispatchContext = createContext<Dispatch | null>(null);
function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, initial);
return (
<StateContext value={state}>
<DispatchContext value={dispatch}>
{children}
</DispatchContext>
</StateContext>
);
}
Why split: components that only need dispatch don’t re-render when state changes.
Memoizing value
// BAD: new object every render → all consumers re-render
<ThemeContext value={{ theme: "dark" }}>
// GOOD
const value = useMemo(() => ({ theme }), [theme]);
<ThemeContext value={value}>
use(Context) in React 19
function Comp({ cond }: { cond: boolean }) {
if (cond) {
const theme = use(ThemeContext); // OK conditionally
return <p>{theme}</p>;
}
return null;
}
use can be called conditionally, unlike useContext.
When NOT to use context
- Frequently-updating state shared widely → render storms. Use a store (Zustand, Jotai, Redux).
- Passing data 1-2 levels deep → just use props.
- “Avoiding prop drilling” — sometimes drilling is fine.
Combining providers
Nest readably:
function Providers({ children }) {
return (
<Theme>
<Auth>
<Router>
{children}
</Router>
</Auth>
</Theme>
);
}
Or compose:
function compose(...providers: React.FC<{ children: React.ReactNode }>[]) {
return ({ children }: { children: React.ReactNode }) =>
providers.reduceRight((acc, P) => <P>{acc}</P>, <>{children}</>);
}
const Providers = compose(Theme, Auth, Router);
Provider with side-effect setup
function AuthProvider({ children }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
return subscribeToAuth((u) => setUser(u));
}, []);
const value = useMemo(() => ({ user }), [user]);
return <AuthContext value={value}>{children}</AuthContext>;
}
Selector pattern
Built-in context doesn’t support selectors. To get them, use:
- use-context-selector: drop-in selector subscription
- zustand: store with subscription-based selectors
// Zustand example
const userId = useStore((s) => s.user.id); // re-renders only on id change
Server vs client context
In RSC: context cannot cross client/server boundary directly. Pass via props or use server-component-friendly patterns.
Common mistakes
- Object literal as
value— re-renders all consumers every render. - Using context for high-frequency state (mouse pos, animation).
- Forgetting provider → consumer reads default value silently.
- Single huge context with everything → un-debuggable re-renders.
- Setting state inside reading component triggering chain re-renders.
Read this next
If you want my context + auth setup, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .