useReducer is an alternative to useState for managing complex state logic. It’s usually preferable when you have state that involves multiple sub-values or when the next state depends on the previous one in complex ways.
Signature
function useReducer<S, A>(
reducer: (prevState: S, action: A) => S,
initialState: S
): [S, Dispatch<A>]
function useReducer<S, A, I>(
reducer: (prevState: S, action: A) => S,
initialArg: I,
init: (arg: I) => S
): [S, Dispatch<A>]
Parameters
reducer
(prevState: S, action: A) => S
required
A function that takes the current state and an action, and returns the new state. The reducer should be a pure function.
The initial state value (when using the two-parameter signature).
The initial argument passed to the init function (when using the three-parameter signature).
A function that receives initialArg and returns the initial state. Useful for lazy initialization or deriving initial state from props.
Returns
Returns a tuple containing:
- Current state (
S): The current state value
- Dispatch function (
Dispatch<A>): A function to dispatch actions to the reducer
Basic Usage
import { useReducer } from 'preact/hooks';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}
With Action Payloads
function reducer(state, action) {
switch (action.type) {
case 'add-item':
return {
...state,
items: [...state.items, action.payload]
};
case 'remove-item':
return {
...state,
items: state.items.filter(item => item.id !== action.payload.id)
};
case 'update-item':
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id ? action.payload : item
)
};
default:
return state;
}
}
function TodoList() {
const [state, dispatch] = useReducer(reducer, { items: [] });
const addTodo = (text) => {
dispatch({
type: 'add-item',
payload: { id: Date.now(), text }
});
};
return (
<div>
{state.items.map(item => (
<div key={item.id}>
{item.text}
<button onClick={() => dispatch({ type: 'remove-item', payload: item })}>
Remove
</button>
</div>
))}
</div>
);
}
Lazy Initialization
function init(initialCount) {
// Expensive computation only runs once
return {
count: initialCount,
history: [initialCount]
};
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
const newCount = state.count + 1;
return {
count: newCount,
history: [...state.history, newCount]
};
case 'reset':
return init(action.payload);
default:
return state;
}
}
function Counter({ initialCount }) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<div>
<p>Count: {state.count}</p>
<p>History: {state.history.join(', ')}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'reset', payload: 0 })}>Reset</button>
</div>
);
}
Complex State Management
const initialState = {
user: null,
loading: false,
error: null,
data: []
};
function reducer(state, action) {
switch (action.type) {
case 'fetch-start':
return { ...state, loading: true, error: null };
case 'fetch-success':
return {
...state,
loading: false,
data: action.payload
};
case 'fetch-error':
return {
...state,
loading: false,
error: action.payload
};
case 'set-user':
return { ...state, user: action.payload };
case 'logout':
return { ...initialState };
default:
return state;
}
}
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
const fetchData = async () => {
dispatch({ type: 'fetch-start' });
try {
const response = await fetch('/api/data');
const data = await response.json();
dispatch({ type: 'fetch-success', payload: data });
} catch (error) {
dispatch({ type: 'fetch-error', payload: error.message });
}
};
return <div>{/* ... */}</div>;
}
Passing Dispatch Down
function reducer(state, action) {
switch (action.type) {
case 'toggle':
return { ...state, [action.payload]: !state[action.payload] };
default:
return state;
}
}
function DeepChild({ dispatch, id }) {
// Child can dispatch without callbacks
return (
<button onClick={() => dispatch({ type: 'toggle', payload: id })}>
Toggle
</button>
);
}
function Parent() {
const [state, dispatch] = useReducer(reducer, {
item1: false,
item2: false
});
return (
<div>
<DeepChild dispatch={dispatch} id="item1" />
<DeepChild dispatch={dispatch} id="item2" />
</div>
);
}
useReducer is usually preferable to useState when:
- You have complex state logic involving multiple sub-values
- The next state depends on the previous state in non-trivial ways
- You want to optimize performance for components with deep updates by passing
dispatch down instead of callbacks
The reducer function should be pure: given the same inputs, it should always return the same output. Don’t perform side effects in reducers.
If you dispatch an action that results in the same state value (compared using Object.is), Preact will skip re-rendering the component and its children.