references/anti-patterns.md
# Context Anti-Patterns
> Common mistakes when using React Context, with explanations and fixes.
---
## Anti-Pattern 1: Unstable Value Object
**Problem**: Creating a new object in the Provider render body causes ALL consumers to re-render on every parent render, even when the actual values have not changed.
```tsx
// BAD -- new object reference every render
function UserProvider({ children }: { children: ReactNode }): JSX.Element {
const [user, setUser] = useState<User | null>(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
```
**Why it breaks**: React compares context values with `Object.is`. A new object literal `{ user, setUser }` creates a new reference every render, so `Object.is(prevValue, nextValue)` returns `false` even when `user` has not changed.
**Fix**: ALWAYS stabilize the value with `useMemo`:
```tsx
// GOOD -- stable reference, re-renders only when user changes
function UserProvider({ children }: { children: ReactNode }): JSX.Element {
const [user, setUser] = useState<User | null>(null);
const value = useMemo(() => ({ user, setUser }), [user]);
return (
<UserContext.Provider value={value}>{children}</UserContext.Provider>
);
}
```
---
## Anti-Pattern 2: Missing Provider Guard
**Problem**: Using `useContext` without checking for a missing Provider returns the `defaultValue`, which silently hides bugs.
```tsx
// BAD -- returns null silently, crashes later with confusing error
const AuthContext = createContext<AuthContextType | null>(null);
function ProfilePage(): JSX.Element {
const auth = useContext(AuthContext);
// auth is null if Provider is missing -- crashes on auth.user access
return <span>{auth.user.name}</span>;
}
```
**Fix**: ALWAYS create a custom hook that throws a descriptive error:
```tsx
// GOOD -- fails fast with clear message
function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (context === null) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
function ProfilePage(): JSX.Element {
const { user } = useAuth(); // throws if AuthProvider is missing
return <span>{user?.name}</span>;
}
```
---
## Anti-Pattern 3: Single Giant Context
**Problem**: Putting all application state into one context causes every consumer to re-render when ANY value changes.
```tsx
// BAD -- theme change re-renders auth consumers, locale consumers, etc.
interface AppContextType {
user: User | null;
theme: Theme;
locale: string;
notifications: Notification[];
settings: Settings;
}
const AppContext = createContext<AppContextType | null>(null);
```
**Why it breaks**: When `notifications` updates, components that only read `theme` still re-render because the context value reference changed.
**Fix**: ALWAYS split contexts by concern:
```tsx
// GOOD -- independent update cycles
const AuthContext = createContext<AuthContextType | null>(null);
const ThemeContext = createContext<ThemeContextType | null>(null);
const LocaleContext = createContext<LocaleContextType | null>(null);
const NotificationContext = createContext<NotificationContextType | null>(null);
```
---
## Anti-Pattern 4: Context for Frequently Changing Values
**Problem**: Using Context for values that change many times per second (mouse position, scroll, animation) causes the entire consumer subtree to re-render at that frequency.
```tsx
// BAD -- re-renders ALL consumers 60 times per second
const MouseContext = createContext<{ x: number; y: number }>({ x: 0, y: 0 });
function MouseTracker({ children }: { children: ReactNode }): JSX.Element {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
function handleMove(e: MouseEvent): void {
setPos({ x: e.clientX, y: e.clientY }); // triggers re-render cascade
}
window.addEventListener("mousemove", handleMove);
return () => window.removeEventListener("mousemove", handleMove);
}, []);
return (
<MouseContext.Provider value={pos}>{children}</MouseContext.Provider>
);
}
```
**Fix**: Use `useSyncExternalStore` or an external store with selectors:
```tsx
// GOOD -- only subscribers to specific values re-render
import { useSyncExternalStore } from "react";
const mouseStore = {
position: { x: 0, y: 0 },
listeners: new Set<() => void>(),
subscribe(listener: () => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
},
getSnapshot(): { x: number; y: number } {
return this.position;
},
};
window.addEventListener("mousemove", (e) => {
mouseStore.position = { x: e.clientX, y: e.clientY };
mouseStore.listeners.forEach((l) => l());
});
function useMousePosition(): { x: number; y: number } {
return useSyncExternalStore(
mouseStore.subscribe.bind(mouseStore),
mouseStore.getSnapshot.bind(mouseStore)
);
}
```
---
## Anti-Pattern 5: Reading Context in the Same Component as Provider
**Problem**: `useContext` searches UPWARD from the calling component. The Provider in the same component does NOT affect that component's own `useContext` call.
```tsx
// BAD -- useTheme reads from PARENT provider, not this component's provider
function App(): JSX.Element {
const theme = useTheme(); // reads from ABOVE, not below
return (
<ThemeContext.Provider value="dark">
<Page />
</ThemeContext.Provider>
);
}
```
**Fix**: Move the consumer into a child component:
```tsx
// GOOD -- consumer is below the provider
function App(): JSX.Element {
return (
<ThemeContext.Provider value="dark">
<ThemedApp />
</ThemeContext.Provider>
);
}
function ThemedApp(): JSX.Element {
const { theme } = useTheme(); // correctly reads "dark"
return <Page className={theme} />;
}
```
---
## Anti-Pattern 6: Using Context.Consumer
**Problem**: The render prop pattern (`Context.Consumer`) is legacy and verbose.
```tsx
// BAD -- legacy pattern, verbose, hard to compose
function Header(): JSX.Element {
return (
<ThemeContext.Consumer>
{(theme) => (
<AuthContext.Consumer>
{(auth) => (
<header className={theme}>
Welcome, {auth.user?.name}
</header>
)}
</AuthContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
```
**Fix**: ALWAYS use `useContext` hook:
```tsx
// GOOD -- clean, composable
function Header(): JSX.Element {
const { theme } = useTheme();
const { user } = useAuth();
return (
<header className={theme}>
Welcome, {user?.name}
</header>
);
}
```
---
## Anti-Pattern 7: Prop Drilling Through Context
**Problem**: Using Context to pass data that only goes one or two levels deep. Context adds complexity without benefit for shallow trees.
```tsx
// BAD -- unnecessary context for shallow tree
const ButtonColorContext = createContext<string>("blue");
function Card(): JSX.Element {
return (
<ButtonColorContext.Provider value="red">
<CardBody />
</ButtonColorContext.Provider>
);
}
function CardBody(): JSX.Element {
const color = useContext(ButtonColorContext);
return <Button color={color} />;
}
```
**Fix**: Pass props directly for shallow hierarchies:
```tsx
// GOOD -- simple prop passing for 1-2 levels
function Card(): JSX.Element {
return <CardBody buttonColor="red" />;
}
function CardBody({ buttonColor }: { buttonColor: string }): JSX.Element {
return <Button color={buttonColor} />;
}
```
**Rule of thumb**: If the data passes through fewer than 3 levels, prefer props over Context.
---
## Anti-Pattern 8: Duplicate Module Instances
**Problem**: If the module containing `createContext` is bundled twice (e.g., different versions in node_modules), the Provider and Consumer reference DIFFERENT context objects.
**Symptoms**:
- `useContext` returns the default value despite Provider being present
- No error messages -- silently fails
**Fix**:
- Ensure single module instance in your bundler configuration
- Use `npm ls react` to check for duplicate React installations
- In monorepos, hoist shared dependencies to the root
---
## Anti-Pattern 9: Missing value Prop on Provider
**Problem**: Rendering a Provider without a `value` prop passes `undefined` to consumers, overriding the `defaultValue` from `createContext`.
```tsx
// BAD -- passes undefined, NOT the defaultValue from createContext
<ThemeContext.Provider>
<App />
</ThemeContext.Provider>
```
**Why**: `defaultValue` from `createContext` is ONLY used when there is NO Provider in the tree at all. A Provider without `value` explicitly passes `undefined`.
**Fix**: ALWAYS pass an explicit `value` prop:
```tsx
// GOOD
<ThemeContext.Provider value={theme}>
<App />
</ThemeContext.Provider>
```
---
## Summary Table
| Anti-Pattern | Impact | Severity |
|-------------|--------|----------|
| Unstable value object | All consumers re-render every parent render | High |
| Missing provider guard | Silent null bugs, confusing runtime errors | High |
| Single giant context | Unrelated components re-render together | Medium |
| Frequently changing values | Performance degradation, janky UI | High |
| Same-component provider/consumer | Reads wrong context value | Medium |
| Using Context.Consumer | Verbose code, callback hell | Low |
| Context for shallow props | Unnecessary complexity | Low |
| Duplicate module instances | Silent context mismatch | High |
| Missing value prop | Consumers receive undefined | Medium |
references/examples.md
# Context API Examples
> Complete, copy-paste-ready patterns for React Context with TypeScript.
---
## Example 1: Theme Context (Simple Toggle)
```tsx
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
type ReactNode,
} from "react";
// Types
type Theme = "light" | "dark";
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
// Context
const ThemeContext = createContext<ThemeContextType | null>(null);
// Custom hook
function useTheme(): ThemeContextType {
const context = useContext(ThemeContext);
if (context === null) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}
// Provider
function ThemeProvider({ children }: { children: ReactNode }): JSX.Element {
const [theme, setTheme] = useState<Theme>("light");
const toggleTheme = useCallback(() => {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
}, []);
const value = useMemo<ThemeContextType>(
() => ({ theme, toggleTheme }),
[theme, toggleTheme]
);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
// Consumer component
function ThemeToggle(): JSX.Element {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
Current theme: {theme}
</button>
);
}
// App
function App(): JSX.Element {
return (
<ThemeProvider>
<ThemeToggle />
</ThemeProvider>
);
}
```
---
## Example 2: Auth Context (Async Operations)
```tsx
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from "react";
// Types
interface User {
id: string;
name: string;
email: string;
}
interface AuthContextType {
user: User | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
}
// Context
const AuthContext = createContext<AuthContextType | null>(null);
// Custom hook
function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (context === null) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
// Provider
function AuthProvider({ children }: { children: ReactNode }): JSX.Element {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
// Check session on mount
useEffect(() => {
let ignore = false;
async function checkSession(): Promise<void> {
try {
const session = await api.getSession();
if (!ignore) {
setUser(session.user);
}
} catch {
if (!ignore) {
setUser(null);
}
} finally {
if (!ignore) {
setIsLoading(false);
}
}
}
checkSession();
return () => {
ignore = true;
};
}, []);
const login = useCallback(async (email: string, password: string) => {
const result = await api.login(email, password);
setUser(result.user);
}, []);
const logout = useCallback(async () => {
await api.logout();
setUser(null);
}, []);
const value = useMemo<AuthContextType>(
() => ({ user, isLoading, login, logout }),
[user, isLoading, login, logout]
);
return (
<AuthContext.Provider value={value}>{children}</AuthContext.Provider>
);
}
// Guard component
function RequireAuth({ children }: { children: ReactNode }): JSX.Element {
const { user, isLoading } = useAuth();
if (isLoading) {
return <LoadingSpinner />;
}
if (user === null) {
return <Navigate to="/login" />;
}
return <>{children}</>;
}
```
---
## Example 3: Context + useReducer (Todo App)
```tsx
import {
createContext,
useContext,
useMemo,
useReducer,
type Dispatch,
type ReactNode,
} from "react";
// Types
interface Todo {
id: number;
text: string;
done: boolean;
}
interface TodosState {
todos: Todo[];
}
type TodoAction =
| { type: "ADD"; text: string }
| { type: "TOGGLE"; id: number }
| { type: "DELETE"; id: number }
| { type: "CLEAR_COMPLETED" };
type TodosDispatch = Dispatch<TodoAction>;
// Reducer
function todosReducer(state: TodosState, action: TodoAction): TodosState {
switch (action.type) {
case "ADD":
return {
...state,
todos: [
...state.todos,
{ id: Date.now(), text: action.text, done: false },
],
};
case "TOGGLE":
return {
...state,
todos: state.todos.map((t) =>
t.id === action.id ? { ...t, done: !t.done } : t
),
};
case "DELETE":
return {
...state,
todos: state.todos.filter((t) => t.id !== action.id),
};
case "CLEAR_COMPLETED":
return {
...state,
todos: state.todos.filter((t) => !t.done),
};
}
}
const initialState: TodosState = { todos: [] };
// Split contexts for performance
const TodosStateContext = createContext<TodosState | null>(null);
const TodosDispatchContext = createContext<TodosDispatch | null>(null);
// Custom hooks
function useTodosState(): TodosState {
const context = useContext(TodosStateContext);
if (context === null) {
throw new Error("useTodosState must be used within a TodosProvider");
}
return context;
}
function useTodosDispatch(): TodosDispatch {
const context = useContext(TodosDispatchContext);
if (context === null) {
throw new Error("useTodosDispatch must be used within a TodosProvider");
}
return context;
}
// Provider
function TodosProvider({ children }: { children: ReactNode }): JSX.Element {
const [state, dispatch] = useReducer(todosReducer, initialState);
return (
<TodosStateContext.Provider value={state}>
<TodosDispatchContext.Provider value={dispatch}>
{children}
</TodosDispatchContext.Provider>
</TodosStateContext.Provider>
);
}
// Consumer: reads state (re-renders when todos change)
function TodoList(): JSX.Element {
const { todos } = useTodosState();
return (
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
// Consumer: reads dispatch only (NEVER re-renders from state changes)
function AddTodoForm(): JSX.Element {
const dispatch = useTodosDispatch();
const [text, setText] = useState("");
function handleSubmit(e: React.FormEvent): void {
e.preventDefault();
if (text.trim()) {
dispatch({ type: "ADD", text: text.trim() });
setText("");
}
}
return (
<form onSubmit={handleSubmit}>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button type="submit">Add</button>
</form>
);
}
```
---
## Example 4: Composing Multiple Providers
```tsx
// Pattern: Provider composer to avoid nesting hell
interface ProviderProps {
children: ReactNode;
}
type ProviderComponent = React.ComponentType<ProviderProps>;
function ComposeProviders({
providers,
children,
}: {
providers: ProviderComponent[];
children: ReactNode;
}): JSX.Element {
return providers.reduceRight<JSX.Element>(
(acc, Provider) => <Provider>{acc}</Provider>,
<>{children}</>
);
}
// Usage
function App(): JSX.Element {
return (
<ComposeProviders
providers={[AuthProvider, ThemeProvider, LocaleProvider, TodosProvider]}
>
<MainLayout />
</ComposeProviders>
);
}
```
---
## Example 5: React 19 -- use(Context) in Conditionals
```tsx
import { use, createContext } from "react";
const FeatureFlagContext = createContext<Record<string, boolean>>({});
// React 19 ONLY -- use() can be called conditionally
function FeatureGate({
flag,
children,
fallback,
}: {
flag: string;
children: ReactNode;
fallback?: ReactNode;
}): JSX.Element {
// ALLOWED: use() inside a conditional
const flags = use(FeatureFlagContext);
if (flags[flag]) {
return <>{children}</>;
}
return <>{fallback ?? null}</>;
}
// With useContext this would require unconditional call:
function FeatureGateReact18({
flag,
children,
fallback,
}: {
flag: string;
children: ReactNode;
fallback?: ReactNode;
}): JSX.Element {
// useContext MUST be called unconditionally
const flags = useContext(FeatureFlagContext);
if (flags[flag]) {
return <>{children}</>;
}
return <>{fallback ?? null}</>;
}
```
---
## Example 6: React 19 -- Context as Provider
```tsx
// React 19: Context renders directly as a provider
function App(): JSX.Element {
const [theme, setTheme] = useState<"light" | "dark">("light");
const value = useMemo(() => ({ theme, setTheme }), [theme]);
// No .Provider needed in React 19
return (
<ThemeContext value={value}>
<Page />
</ThemeContext>
);
}
// React 18: must use .Provider
function AppReact18(): JSX.Element {
const [theme, setTheme] = useState<"light" | "dark">("light");
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
<Page />
</ThemeContext.Provider>
);
}
```
---
## Example 7: Locale Context with Nested Override
```tsx
interface LocaleContextType {
locale: string;
t: (key: string) => string;
}
const LocaleContext = createContext<LocaleContextType | null>(null);
function useLocale(): LocaleContextType {
const context = useContext(LocaleContext);
if (context === null) {
throw new Error("useLocale must be used within a LocaleProvider");
}
return context;
}
function LocaleProvider({
locale,
children,
}: {
locale: string;
children: ReactNode;
}): JSX.Element {
const translations = useTranslations(locale);
const value = useMemo<LocaleContextType>(
() => ({
locale,
t: (key: string) => translations[key] ?? key,
}),
[locale, translations]
);
return (
<LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>
);
}
// Nested override: admin panel uses English regardless of app locale
function App(): JSX.Element {
return (
<LocaleProvider locale="nl">
<MainContent />
<LocaleProvider locale="en">
<AdminPanel /> {/* Always English */}
</LocaleProvider>
</LocaleProvider>
);
}
```
SKILL.md
---
name: react-syntax-context
description: >
Use when sharing state across components without prop drilling, implementing
theme/auth/locale providers, or optimizing context performance. Prevents the
common mistake of putting frequently-changing values in context causing
unnecessary re-renders. Covers createContext, useContext, Provider pattern,
TypeScript generics, default values, multiple contexts, performance.
Keywords: createContext, useContext, Provider, context, prop drilling, theme, share data between components, avoid prop drilling, theme provider, auth context, global data..
license: MIT
compatibility: "Designed for Claude Code. Requires React 18.x or 19.x with TypeScript."
metadata:
author: OpenAEC-Foundation
version: "1.0"
---
# react-syntax-context
## Quick Reference
### Context API Surface
| API | Purpose | Version |
|-----|---------|---------|
| `createContext<T>(defaultValue)` | Create a context object with TypeScript generic | React 18+ |
| `useContext(Context)` | Consume the nearest Provider value | React 18+ |
| `use(Context)` | Consume context inside conditionals/loops | React 19 only |
| `<Context.Provider value={...}>` | Provide value to descendants | React 18 |
| `<Context value={...}>` | Provide value to descendants (no `.Provider`) | React 19 |
### Critical Warnings
**NEVER** create a new object literal directly in the Provider `value` prop without `useMemo` -- this creates a new reference every render and forces ALL consumers to re-render.
**NEVER** use Context for frequently changing values (e.g., mouse position, scroll offset, animation frames) -- use `useSyncExternalStore` or an external state library instead.
**NEVER** put everything in a single global context -- split by concern (theme, auth, locale) to prevent unrelated re-renders.
**ALWAYS** provide a custom hook wrapper (e.g., `useAuth()`) around `useContext` -- this centralizes the missing-provider check and improves API ergonomics.
**ALWAYS** use `useMemo` to stabilize context value objects -- this prevents unnecessary consumer re-renders when the provider's parent re-renders.
---
## Decision Tree: Do You Need Context?
```
Need to share state across components?
├── Only 1-2 levels deep? → Pass props directly (no Context needed)
├── Many levels deep but rarely changes? → Use Context
├── Changes frequently (>1x per second)? → Use external store (Zustand, Jotai, useSyncExternalStore)
├── Server-only data (no interactivity)? → Pass as props from Server Component
└── Complex state with actions? → Context + useReducer
```
---
## Creating Context with TypeScript
### Pattern: Null Default with Type Assertion
```tsx
import { createContext, useContext, type ReactNode } from "react";
// 1. Define the context type
interface AuthContextType {
user: User | null;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
}
// 2. Create with null default -- ALWAYS use this pattern for contexts
// that REQUIRE a provider
const AuthContext = createContext<AuthContextType | null>(null);
// 3. Custom hook with missing-provider guard
function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (context === null) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
```
**Why null default?** Passing a "real" default value hides bugs where a Provider is missing. The null pattern forces an explicit error at the call site.
### Pattern: Safe Default (No Provider Required)
```tsx
// Use when a sensible default exists and Provider is optional
const ThemeContext = createContext<"light" | "dark">("light");
// No null check needed -- always returns a valid value
function useTheme(): "light" | "dark" {
return useContext(ThemeContext);
}
```
---
## Provider Patterns
### Custom Provider Component
ALWAYS encapsulate state logic inside a custom Provider component:
```tsx
interface AuthProviderProps {
children: ReactNode;
}
function AuthProvider({ children }: AuthProviderProps): JSX.Element {
const [user, setUser] = useState<User | null>(null);
const login = useCallback(async (credentials: Credentials) => {
const result = await authApi.login(credentials);
setUser(result.user);
}, []);
const logout = useCallback(() => {
setUser(null);
authApi.logout();
}, []);
// ALWAYS stabilize the value object with useMemo
const value = useMemo<AuthContextType>(
() => ({ user, login, logout }),
[user, login, logout]
);
// React 19: <AuthContext value={value}>
// React 18: <AuthContext.Provider value={value}>
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
```
### Nested Providers
The closest Provider wins. Inner providers override outer ones:
```tsx
function App(): JSX.Element {
return (
<ThemeContext.Provider value="dark">
<Sidebar />
<ThemeContext.Provider value="light">
<MainContent /> {/* reads "light" */}
</ThemeContext.Provider>
</ThemeContext.Provider>
);
}
```
---
## Multiple Contexts: Split by Concern
ALWAYS split unrelated concerns into separate contexts:
```tsx
// GOOD -- independent contexts
const ThemeContext = createContext<ThemeContextType | null>(null);
const AuthContext = createContext<AuthContextType | null>(null);
const LocaleContext = createContext<LocaleContextType | null>(null);
// Compose providers at app root
function AppProviders({ children }: { children: ReactNode }): JSX.Element {
return (
<AuthProvider>
<ThemeProvider>
<LocaleProvider>
{children}
</LocaleProvider>
</ThemeProvider>
</AuthProvider>
);
}
```
**Why split?** When `user` changes in a combined context, components that only need `theme` still re-render. Separate contexts prevent this.
---
## Performance: Splitting Read and Write Contexts
For state + dispatch patterns, split into TWO contexts to prevent dispatch-only consumers from re-rendering when state changes:
```tsx
const TodosStateContext = createContext<TodosState | null>(null);
const TodosDispatchContext = createContext<TodosDispatch | null>(null);
function TodosProvider({ children }: { children: ReactNode }): JSX.Element {
const [state, dispatch] = useReducer(todosReducer, initialState);
// State changes every update -- only state consumers re-render
// Dispatch is stable -- dispatch consumers NEVER re-render from state changes
return (
<TodosStateContext.Provider value={state}>
<TodosDispatchContext.Provider value={dispatch}>
{children}
</TodosDispatchContext.Provider>
</TodosStateContext.Provider>
);
}
// Targeted hooks
function useTodosState(): TodosState {
const ctx = useContext(TodosStateContext);
if (ctx === null) throw new Error("useTodosState requires TodosProvider");
return ctx;
}
function useTodosDispatch(): TodosDispatch {
const ctx = useContext(TodosDispatchContext);
if (ctx === null) throw new Error("useTodosDispatch requires TodosProvider");
return ctx;
}
```
Components that only call `useTodosDispatch()` will NOT re-render when todos state changes.
---
## Context + useReducer
ALWAYS prefer `useReducer` over `useState` when context manages complex state with multiple actions:
```tsx
type TodoAction =
| { type: "ADD"; text: string }
| { type: "TOGGLE"; id: number }
| { type: "DELETE"; id: number };
interface TodosState {
todos: Todo[];
filter: "all" | "active" | "completed";
}
type TodosDispatch = React.Dispatch<TodoAction>;
function todosReducer(state: TodosState, action: TodoAction): TodosState {
switch (action.type) {
case "ADD":
return {
...state,
todos: [...state.todos, { id: Date.now(), text: action.text, done: false }],
};
case "TOGGLE":
return {
...state,
todos: state.todos.map((t) =>
t.id === action.id ? { ...t, done: !t.done } : t
),
};
case "DELETE":
return {
...state,
todos: state.todos.filter((t) => t.id !== action.id),
};
}
}
```
---
## Value Stabilization with useMemo
```tsx
// BAD -- new object every render, ALL consumers re-render
function ThemeProvider({ children }: { children: ReactNode }): JSX.Element {
const [theme, setTheme] = useState<"light" | "dark">("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// GOOD -- stable reference, consumers re-render only when theme changes
function ThemeProvider({ children }: { children: ReactNode }): JSX.Element {
const [theme, setTheme] = useState<"light" | "dark">("light");
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
```
---
## React 19: use(Context) and Context as Provider
### use(Context) -- Conditional Context Reading
```tsx
// React 19 ONLY -- use() can be called inside conditionals
function StatusBadge({ showAuth }: { showAuth: boolean }): JSX.Element {
if (showAuth) {
// ALLOWED with use() -- FORBIDDEN with useContext()
const { user } = use(AuthContext);
return <Badge user={user} />;
}
return <GuestBadge />;
}
```
### Context as Provider (No .Provider)
```tsx
// React 19 -- render Context directly
<ThemeContext value={theme}>
<App />
</ThemeContext>
// React 18 -- must use .Provider
<ThemeContext.Provider value={theme}>
<App />
</ThemeContext.Provider>
```
React 19 will deprecate `<Context.Provider>` in a future minor release.
---
## When NOT to Use Context
| Scenario | Why Not Context | Use Instead |
|----------|----------------|-------------|
| Frequently changing values (>1x/sec) | Every change re-renders ALL consumers | `useSyncExternalStore`, Zustand, Jotai |
| Large global state (100+ fields) | Single update triggers widespread re-renders | External state library with selectors |
| Animation values | 60fps updates re-render entire consumer tree | CSS variables, refs, animation libraries |
| Form state across many fields | Each keystroke re-renders all field consumers | React Hook Form, Formik |
---
## Reference Links
- [references/examples.md](references/examples.md) -- Complete context patterns with TypeScript
- [references/anti-patterns.md](references/anti-patterns.md) -- Common context mistakes and fixes
### Official Sources
- https://react.dev/reference/react/createContext
- https://react.dev/reference/react/useContext
- https://react.dev/reference/react/use
- https://react.dev/learn/passing-data-deeply-with-context
- https://react.dev/learn/scaling-up-with-reducer-and-context