Pick a depth. Each prompt opens in your AI pre-loaded with the lesson. Click a row to preview the prompt.
useReducer separates state from update logic. Where useState works well for independent values, useReducer shines when the next state depends on multiple pieces of current state, when several actions share similar logic, or when you need to co-locate all valid state transitions in one place. The dispatch function is referentially stable across renders — unlike setState callbacks, you can pass dispatch down through a deep component tree via context without wrapping it in useCallback, because React guarantees it never changes.
useReducer takes a reducer function and initial state. State updates are expressed as dispatched actions. The dispatch function identity is stable — safe to pass down without memoization.
fetchReducer outside the component and write unit tests for it in isolation. Pass each action type and assert the next state. This is the key ergonomic advantage over useState — pure functions are trivially testable.status, data, error). Count how many places you set multiple pieces of state together. Notice how useReducer centralizes those transitions into one function.dispatch down to a deeply nested child via Context. Confirm that dispatch is referentially stable (the child doesn't re-render when the parent's other state changes). Compare to passing a setStatus callback and add useCallback — dispatch requires no wrapping.import { useReducer } from 'react';
type Status = 'idle' | 'loading' | 'success' | 'error';
interface FetchState {
status: Status;
data: string | null;
error: string | null;
}
type FetchAction =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; payload: string }
| { type: 'FETCH_ERROR'; payload: string }
| { type: 'RESET' };
function fetchReducer(state: FetchState, action: FetchAction): FetchState {
switch (action.type) {
case 'FETCH_START':
return { status: 'loading', data: null, error: null };
case 'FETCH_SUCCESS':
return { status: 'success', data: action.payload, error: null };
case 'FETCH_ERROR':
return { status: 'error', data: null, error: action.payload };
case 'RESET':
return { status: 'idle', data: null, error: null };
default:
return state;
}
}
const initialState: FetchState = { status: 'idle', data: null, error: null };
export function FetchButton() {
const [state, dispatch] = useReducer(fetchReducer, initialState);
async function handleFetch() {
dispatch({ type: 'FETCH_START' });
try {
await new Promise(r => setTimeout(r, 800));
dispatch({ type: 'FETCH_SUCCESS', payload: 'Hello from server' });
} catch {
dispatch({ type: 'FETCH_ERROR', payload: 'Network error' });
}
}
return (
<div>
<button onClick={handleFetch} disabled={state.status === 'loading'}>
{state.status === 'loading' ? 'Loading…' : 'Fetch'}
</button>
{state.data && <p>{state.data}</p>}
{state.error && <p style={{ color: 'red' }}>{state.error}</p>}
{state.status !== 'idle' && (
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
)}
</div>
);
}