Manage complex React state by combining useReducer with Context API to avoid prop drilling and centralize state logic across component trees.
Implement scalable state management in React applications by combining `useReducer` with the Context API. This pattern eliminates prop drilling, centralizes state update logic, and makes state accessible throughout your component tree.
This skill guides you through refactoring React components to use the Reducer + Context pattern for managing complex state. You'll create context providers that wrap your application, define reducer functions for state transitions, and consume context in child components without passing props through intermediate layers.
First, analyze the component that currently manages state with `useState` or `useReducer`:
Define a reducer function that handles all state transitions:
```typescript
function reducer(state, action) {
switch (action.type) {
case 'added':
return [...state, { id: action.id, text: action.text, done: false }];
case 'changed':
return state.map(item =>
item.id === action.item.id ? action.item : item
);
case 'deleted':
return state.filter(item => item.id !== action.id);
default:
throw Error('Unknown action: ' + action.type);
}
}
```
**Guidelines:**
Create a separate file (e.g., `StateContext.tsx`) with two contexts:
```typescript
import { createContext } from 'react';
// Context for reading state
export const StateContext = createContext(null);
// Context for dispatching actions
export const DispatchContext = createContext(null);
```
**Why two contexts?**
In the same context file, create a provider that wraps both contexts:
```typescript
import { useReducer } from 'react';
export function StateProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
```
**Best practices:**
In your root component, wrap the component tree:
```typescript
import { StateProvider } from './StateContext';
export default function App() {
return (
<StateProvider>
<Header />
<MainContent />
<Footer />
</StateProvider>
);
}
```
Create custom hooks for type-safe context consumption:
```typescript
import { useContext } from 'react';
export function useState() {
const context = useContext(StateContext);
if (context === null) {
throw new Error('useState must be used within StateProvider');
}
return context;
}
export function useDispatch() {
const context = useContext(DispatchContext);
if (context === null) {
throw new Error('useDispatch must be used within StateProvider');
}
return context;
}
```
Then use in components:
```typescript
import { useState, useDispatch } from './StateContext';
function ItemList() {
const items = useState();
const dispatch = useDispatch();
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.text}
<button onClick={() => dispatch({ type: 'deleted', id: item.id })}>
Delete
</button>
</li>
))}
</ul>
);
}
```
After implementing context, remove unnecessary prop passing:
**Before:**
```typescript
<ParentComponent
state={state}
onAdd={handleAdd}
onDelete={handleDelete}
/>
```
**After:**
```typescript
<ParentComponent />
```
Child components now access state and dispatch directly from context.
```
src/
├── contexts/
│ └── TasksContext.tsx # Context, provider, reducer, hooks
├── components/
│ ├── TaskList.tsx # Consumes context
│ ├── AddTask.tsx # Consumes context
│ └── Task.tsx # Consumes context
└── App.tsx # Wraps tree with provider
```
```typescript
import { createContext, useContext, useReducer, ReactNode } from 'react';
type Task = {
id: number;
text: string;
done: boolean;
};
type Action =
| { type: 'added'; id: number; text: string }
| { type: 'changed'; task: Task }
| { type: 'deleted'; id: number };
const TasksContext = createContext<Task[] | null>(null);
const TasksDispatchContext = createContext<React.Dispatch<Action> | null>(null);
function tasksReducer(tasks: Task[], action: Action): Task[] {
switch (action.type) {
case 'added':
return [...tasks, { id: action.id, text: action.text, done: false }];
case 'changed':
return tasks.map(t => t.id === action.task.id ? action.task : t);
case 'deleted':
return tasks.filter(t => t.id !== action.id);
default:
throw Error('Unknown action type');
}
}
const initialTasks: Task[] = [];
export function TasksProvider({ children }: { children: ReactNode }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
export function useTasks() {
const context = useContext(TasksContext);
if (context === null) {
throw new Error('useTasks must be used within TasksProvider');
}
return context;
}
export function useTasksDispatch() {
const context = useContext(TasksDispatchContext);
if (context === null) {
throw new Error('useTasksDispatch must be used within TasksProvider');
}
return context;
}
```
1. **Verify state updates:** Dispatch actions from child components and confirm UI updates
2. **Check re-render optimization:** Components consuming only dispatch shouldn't re-render on state changes
3. **Test error boundaries:** Verify helpful errors when context is used outside provider
4. **Validate TypeScript types:** Ensure full type safety for state and actions
Leave a review
No reviews yet. Be the first to review this skill!
# Download SKILL.md from killerskills.ai/api/skills/react-reducer-context-pattern/raw