useReducer is useState with the update logic moved out of the event handlers. You write a reducer — a pure function (state, action) => nextState — and the Hook returns the current state plus a dispatch function. Handlers no longer compute anything; they announce what happened.
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@8.0.5/babel.min.js"></script>
<script type="importmap">
{ "imports": { "react": "https://esm.sh/react@19.3.0",
"react-dom/client": "https://esm.sh/react-dom@19.3.0/client" } }
</script>
<script type="text/babel" data-type="module">
import React, { useReducer } from 'react';
import { createRoot } from 'react-dom/client';
function reducer(state, action) {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'reset': return { count: 0 };
default: throw new Error('Unknown action: ' + action.type);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return <p style={{ font: '16px system-ui' }}>Count: {state.count}{' '}
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>{' '}
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button></p>;
}
createRoot(document.getElementById('root')).render(<Counter />);
setTimeout(() => [0, 0, 0].forEach(() =>
document.querySelectorAll('button')[0].click()), 600); // three scripted clicks
</script>
Every way this state can change is now visible in five lines, which is the real payoff: with useState the same transitions would be spread across the handlers, and the next one would be easy to miss. Prefer a reducer when the next state depends on the previous one, when several values move together, or when the same transition fires from more than one place. Keep useState for a checkbox.
Two behaviors follow from the reducer being pure. React 7,897 compares the returned state with the current one using Object.is and skips the re-render when they match, so mutating state in place and returning it does nothing at all. And in Strict Mode, development builds call the reducer twice per dispatch to expose exactly that kind of impurity. dispatch itself has a stable identity: safe to pass down, safe to omit from an Effect's dependencies.