{"id":116,"date":"2026-10-06T05:50:04","date_gmt":"2026-10-06T05:50:04","guid":{"rendered":"https:\/\/webdevpuneet.com\/blog\/?p=116"},"modified":"2026-10-06T05:50:08","modified_gmt":"2026-10-06T05:50:08","slug":"react-usestate-vs-usereducer-when-should-you-use-each","status":"publish","type":"post","link":"https:\/\/webdevpuneet.com\/blog\/react-usestate-vs-usereducer-when-should-you-use-each\/","title":{"rendered":"React useState vs useReducer: When Should You Use Each?"},"content":{"rendered":"<p><!--\n================================================================\n  POST TITLE (paste into the WordPress title field \u2014 it becomes the page H1)\n  React useState vs useReducer: When Should You Use Each?\n\n  SEO TITLE (51 chars, target 50\u201360, max 60 \u2014 \"Optimize SEO\" panel \u2192 SEO TITLE)\n  React useState vs useReducer: When to Use Each Hook\n\n  SEO DESCRIPTION (159 chars, target 150\u2013160, max 160 \u2014 \"Optimize SEO\" panel \u2192 SEO DESCRIPTION)\n  React useState vs useReducer explained with 10 live demos: action logs, a state machine, undo\/redo, a cart reducer and a quiz that tells you which hook to use.\n\n  HOW TO PASTE: click the first line after this comment, press Ctrl+Shift+End, Ctrl+C.\n  In WordPress click \"Type \/ to choose a block\", then Ctrl+V.\n  Fallback: \u22ee menu \u2192 Code editor \u2192 paste.\n================================================================\n--><\/p>\n\n\n<p class=\"wp-block-paragraph\">Both hooks do the exact same job \u2014 they hold state and trigger a re-render when it changes. The real question isn&#8217;t &#8220;which one is more powerful,&#8221; it&#8217;s <strong>how you want to describe an update<\/strong>: with <strong>useState<\/strong>, the caller hands over the next value directly \u2014 <code>setCount(count + 1)<\/code> says &#8220;the count is now this.&#8221; With <strong>useReducer<\/strong>, the caller hands over a description of what happened \u2014 <code>dispatch({ type: 'increment' })<\/code> says &#8220;this occurred,&#8221; and one function, the reducer, is the only place that decides what it means for the state. Once that distinction clicks, most &#8220;which hook do I reach for&#8221; questions answer themselves.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The 10 demos below are all real, running React \u2014 click a button, dispatch an action, watch a log \u2014 so you can see each hook&#8217;s actual behavior instead of reading about it secondhand. Several place the same feature under both hooks so the difference isn&#8217;t a claim, it&#8217;s something you can click through and feel.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Every embed also has a <strong>Fork &amp; Edit<\/strong> button that opens the exact snippet, already running, in <a href=\"https:\/\/webdevpuneet.com\/ui-snippets\/mycode\/\">My Code<\/a> \u2014 webdevpuneet&#8217;s free in-browser editor. Swap a <code>useState<\/code> call for a <code>useReducer<\/code> one on a real component and watch what changes \u2014 that&#8217;s a faster way to build real intuition than any explanation. One note on the code you&#8217;ll see: these demos skip a build step, so the live JS uses <code>React.createElement<\/code> directly instead of JSX \u2014 the logic is identical to the JSX version in every rule snippet below, just without a compiler in between.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The one-sentence rule<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ useState: the caller computes and hands over the next value\nconst &#91;count, setCount] = useState(0);\nsetCount(count + 1);\n\n\/\/ useReducer: the caller describes what happened; the reducer decides the rest\nconst &#91;state, dispatch] = useReducer(reducer, { count: 0 });\ndispatch({ type: 'increment' });<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>useState<\/code> is a direct pipe: you read the current value, compute the next one yourself, and hand it to the setter. That&#8217;s ideal when the &#8220;next value&#8221; logic is genuinely simple and lives comfortably next to wherever you&#8217;re calling it from. <code>useReducer<\/code> adds one layer of indirection \u2014 a pure <code>(state, action) =&gt; newState<\/code> function that owns every transition \u2014 which pays for itself the moment there&#8217;s more than one way to reach the same state, more than one field that needs to change together, or logic complex enough that you&#8217;d like to read it, and test it, in one place instead of scattered across event handlers.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. Same Counter, Two Hooks<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Flip the toggle \u2014 the exact same counter UI, rebuilt twice: once with useState, once with useReducer. Click +, \u2212, and Reset on each side.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/05\/1.html\" style=\"width:100%;height:540px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"Same Counter, Two Hooks Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ useState version\nconst &#91;count, setCount] = useState(0);\nsetCount(count + 1);   \/\/ caller computes the next value\n\n\/\/ useReducer version\nconst &#91;state, dispatch] = useReducer(reducer, { count: 0 });\ndispatch({ type: 'increment' });  \/\/ caller only describes what happened<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Both versions render identically and behave identically to a user \u2014 that&#8217;s the point. The useState version&#8217;s three buttons each know exactly what the next count should be and say so directly. The useReducer version&#8217;s three buttons don&#8217;t calculate anything; they just name an action, and a single <code>reducer<\/code> function elsewhere is the only code that ever decides what <code>'increment'<\/code>, <code>'decrement'<\/code>, or <code>'reset'<\/code> actually do to the state. For a counter this simple, that indirection is genuinely optional \u2014 which is exactly why it&#8217;s the clearest place to see it side by side before it starts mattering.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> building the base mental model before looking at either hook&#8217;s harder cases. <strong>Tip:<\/strong> a reducer with a <code>default: return state<\/code> case is a defensive habit worth keeping even in a trivial reducer like this one \u2014 dispatching an unrecognized action type should never throw or silently produce <code>undefined<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. useState \u2014 One Value, One Setter<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Click the switch. A single independent boolean is the case useState was built for.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/05\/2.html\" style=\"width:100%;height:550px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"useState One Value One Setter Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const &#91;on, setOn] = useState(false);\nconst &#91;clicks, setClicks] = useState(0);\n\nsetOn(prev =&gt; !prev);\nsetClicks(c =&gt; c + 1);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This demo genuinely has two pieces of state \u2014 whether the light is on, and how many times it&#8217;s been clicked \u2014 but they don&#8217;t <em>need<\/em> to be one piece of state, because nothing about updating one depends on reading the other. Each <code>useState<\/code> call is fully self-contained, and the functional-updater form (<code>prev =&gt; !prev<\/code>) means neither setter even needs to close over the current value to stay correct across rapid clicks. Reaching for <code>useReducer<\/code> here wouldn&#8217;t be wrong, exactly \u2014 it would just be solving a coordination problem that doesn&#8217;t exist.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> toggles, single counters, an open\/closed flag, a hovered\/focused flag \u2014 any value whose next state is a short, self-contained calculation. <strong>Tip:<\/strong> the functional updater form, <code>setOn(prev =&gt; !prev)<\/code>, is worth defaulting to whenever the next value depends on the current one \u2014 it sidesteps stale-closure bugs that direct-value updates can hit inside fast event handlers or effects.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. useReducer&#8217;s Action Log<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Every button dispatches a plain object describing what happened. Watch the log \u2014 it&#8217;s a readable history of exactly what this component did, in order.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/05\/3.html\" style=\"width:100%;height:500px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"useReducer Action Log Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function reducer(state, action) {\n  switch (action.type) {\n    case 'increment':   return { count: state.count + 1 };\n    case 'decrement':   return { count: state.count - 1 };\n    case 'incrementBy': return { count: state.count + action.payload };\n    case 'reset':       return { count: 0 };\n    default:            return state;\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The log in this demo isn&#8217;t a special feature of <code>useReducer<\/code> \u2014 it&#8217;s just printing the exact objects each button dispatches, before the reducer ever sees them. That&#8217;s the underlying benefit made visible: because every update is expressed as a small, named, serializable object rather than an inline calculation, you get a free audit trail of &#8220;what happened&#8221; for nothing extra. This is the same shape of information tools like Redux DevTools build an entire time-travel debugger on top of \u2014 <code>useReducer<\/code> gives you the raw ingredient without needing a library.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> any state where knowing <em>what sequence of things happened<\/em> is genuinely useful for debugging, analytics, or logging \u2014 not just the current value. <strong>Tip:<\/strong> action objects with a <code>payload<\/code> field, like <code>incrementBy<\/code> here, are a common and readable convention \u2014 <code>{ type, payload }<\/code> \u2014 but nothing enforces it; the shape of an action is entirely up to your reducer.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. The Bug Multiple useState Calls Invite<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Two counters that are supposed to always move together. Click &#8220;Increment Both&#8221; a few times, then &#8220;Reset&#8221; \u2014 the useState version drifts out of sync. Then flip the toggle and try the same clicks on the reducer version.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/05\/4.html\" style=\"width:100%;height:690px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"The Bug Multiple useState Calls Invite Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ two separate setters \u2014 easy to update only one by mistake\nfunction reset() {\n  setA(0);   \/\/ bug: copy-pasted from a single counter, forgot setB(0)\n}\n\n\/\/ one transition resets both fields from one source of truth\ndispatch({ type: 'reset' });   \/\/ the reducer returns { a: 0, b: 0 }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is a real, common bug, not a contrived one: two <code>useState<\/code> calls that are supposed to stay in lockstep, and a reset handler copy-pasted from a single-counter version that only calls one of the two setters. Nothing in the type system or in React itself catches it \u2014 <code>setA(0)<\/code> is perfectly valid code, and the bug only shows up as behavior, after a few clicks. The reducer version can&#8217;t make this particular mistake, because there&#8217;s no second setter to forget \u2014 the <code>'reset'<\/code> case returns <code>{ a: 0, b: 0 }<\/code> as one new object, from the one function that&#8217;s allowed to touch either field.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> recognizing the actual failure mode useReducer exists to prevent \u2014 not &#8220;useState is bad at counters,&#8221; but &#8220;state that must change together shouldn&#8217;t live in separate setters.&#8221; <strong>Tip:<\/strong> if you ever catch yourself calling more than one setter inside the same event handler for values that are supposed to represent one coherent thing, that&#8217;s usually the tell that they belong in one <code>useReducer<\/code> (or one <code>useState<\/code> holding an object) instead.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">5. A Reducer as a State Machine<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Click &#8220;Next&#8221; to advance the light through its only three legal transitions. There&#8217;s no path to an invalid color.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/05\/5.html\" style=\"width:100%;height:700px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"A Reducer as a State Machine Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const TRANSITIONS = { red: 'green', green: 'yellow', yellow: 'red' };\n\nfunction reducer(state, action) {\n  switch (action.type) {\n    case 'next': return TRANSITIONS&#91;state];\n    default:     return state;\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">There&#8217;s exactly one action this reducer understands, and exactly one legal next state for each current state \u2014 the traffic light can never jump from red straight to yellow, or sit in some in-between value, because <code>TRANSITIONS[state]<\/code> simply doesn&#8217;t have an entry for that. This is <code>useReducer<\/code> used as what it actually is: a small, explicit state machine. Trying to build the same guarantee with <code>useState<\/code> would mean re-deriving &#8220;what&#8217;s a valid next color&#8221; inline, every single place the light can change \u2014 and hoping every call site agrees.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> anything with a genuinely fixed set of states and a genuinely fixed set of legal transitions between them \u2014 a wizard&#8217;s steps, a media player&#8217;s play\/pause\/buffering states, an order&#8217;s pending\/shipped\/delivered\/cancelled lifecycle. <strong>Tip:<\/strong> when the transition table grows past a handful of states, a library like XState builds on exactly this pattern with visualization and stricter guarantees \u2014 but the core idea, a lookup table plus a reducer, is often all a UI actually needs.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">6. Undo\/Redo, Free With a Reducer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Change the color a few times, then hit Undo. The reducer keeps past and future stacks alongside the present value.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/05\/6.html\" style=\"width:100%;height:470px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"Undo Redo Free With a Reducer Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function reducer(state, action) {\n  switch (action.type) {\n    case 'set':  return { past: &#91;...state.past, state.present], present: action.color, future: &#91;] };\n    case 'undo': return { past: state.past.slice(0, -1), present: state.past.at(-1), future: &#91;state.present, ...state.future] };\n    case 'redo': return { past: &#91;...state.past, state.present], present: state.future&#91;0], future: state.future.slice(1) };\n    default:     return state;\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Undo\/redo is the demo where the gap between the two hooks stops being subtle. The state here isn&#8217;t just &#8220;the current color&#8221; \u2014 it&#8217;s <em>the current color plus its entire history<\/em>, and every single change (setting a new color, undoing, redoing) has to correctly shuffle three arrays in relation to each other. Writing this with scattered <code>useState<\/code> calls would mean juggling three separate setters that all have to update in the same tick, in the correct order, from handlers all over the component. As one reducer, it&#8217;s one function, and every transition is a single, self-contained case.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> undo\/redo, multi-step wizards where &#8220;back&#8221; needs to restore exact prior state, draft\/version history \u2014 any feature where &#8220;what was the state before this&#8221; is itself part of what you&#8217;re modeling. <strong>Tip:<\/strong> this exact <code>{ past, present, future }<\/code> shape is a well-known enough pattern that it has a name \u2014 a &#8220;history reducer&#8221; \u2014 and it composes: you can wrap <em>any<\/em> existing reducer in one generically to add undo\/redo to it without touching its original logic.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">7. Independent Fields Don&#8217;t Need a Reducer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Type in either field, or flip the checkbox \u2014 three plain useState calls, none of them aware the others exist.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/05\/7.html\" style=\"width:100%;height:510px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"Independent Fields Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const &#91;name, setName] = useState('');\nconst &#91;email, setEmail] = useState('');\nconst &#91;subscribe, setSubscribe] = useState(false);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">It&#8217;s tempting, once <code>useReducer<\/code> is on your radar, to reach for it every time a component has &#8220;more than one piece of state&#8221; \u2014 but a form is exactly the case where that instinct usually backfires. These three fields don&#8217;t validate against each other, don&#8217;t derive from each other, and no single update ever needs to touch more than one of them. Wrapping them in a reducer would mean writing <code>dispatch({ type: 'setName', value })<\/code> at every keystroke for no behavioral gain \u2014 more code expressing exactly the same three independent facts.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> forms (up to a point), any UI region where each value&#8217;s next state is computed from nothing but itself and the new input. <strong>Tip:<\/strong> the line does move \u2014 a form with five-plus interdependent fields, cross-field validation, or a submit path that needs to reset everything atomically is a legitimate signal to consolidate into either one <code>useState({...})<\/code> object or a <code>useReducer<\/code>, precisely because the fields have stopped being independent.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">8. One Reducer, One Cart<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Add items, bump quantities, remove a line \u2014 every update to this nested array runs through one function.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/05\/8.html\" style=\"width:100%;height:500px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"One Reducer One Cart Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function reducer(items, action) {\n  switch (action.type) {\n    case 'add':       return items.some(i =&gt; i.id === action.id)\n                                   ? items.map(i =&gt; i.id === action.id ? { ...i, qty: i.qty + 1 } : i)\n                                   : &#91;...items, { ...action.item, qty: 1 }];\n    case 'changeQty': return items.map(i =&gt; i.id === action.id ? { ...i, qty: i.qty + action.delta } : i)\n                                   .filter(i =&gt; i.qty &gt; 0);\n    case 'remove':    return items.filter(i =&gt; i.id !== action.id);\n    default:          return items;\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A shopping cart is a nested array of objects, and updating it immutably \u2014 &#8220;increment this one item&#8217;s quantity without mutating the array or the object&#8221; \u2014 takes a few lines of <code>map<\/code>\/<code>filter<\/code>\/spread every time you do it. With <code>useState<\/code>, that logic tends to get rewritten (slightly differently, and with slightly different bugs) inside every handler that touches the cart: one version inside &#8220;add to cart,&#8221; a subtly different one inside &#8220;change quantity.&#8221; Here it&#8217;s written exactly once, inside the reducer, and every call site just describes intent \u2014 <code>dispatch({ type: 'add', id })<\/code> \u2014 and trusts the reducer to handle the immutable-update mechanics correctly, the same way, every time.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> collections \u2014 carts, todo lists, kanban boards, multi-select tables \u2014 any state shaped like an array or object of records where multiple different operations (add, remove, update-one) all need to preserve immutability correctly. <strong>Tip:<\/strong> because the reducer here is a plain function with no React inside it, you can copy its body into a unit test and assert <code>reducer(items, { type: 'add', id: 'p1' })<\/code> directly \u2014 no component render, no DOM, no testing-library setup required.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">9. A Reducer Is Just a Function \u2014 Replay It<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">&#8220;Replay&#8221; feeds the same six actions into the reducer every time, with no clicking involved. Same actions, same result.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/05\/9.html\" style=\"width:100%;height:530px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"A Reducer Is Just a Function Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const SCRIPT = &#91;\n  { type: 'increment' }, { type: 'increment' }, { type: 'incrementBy', payload: 5 },\n  { type: 'decrement' }, { type: 'increment' }, { type: 'reset' }\n];\n\nSCRIPT.forEach(action =&gt; dispatch(action));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This demo doesn&#8217;t do anything a user would normally trigger \u2014 it&#8217;s here to make one property concrete: a reducer is a pure function of <code>(state, action) =&gt; newState<\/code>, with no dependency on anything else in the component. Feed it the same starting state and the same sequence of actions, and you get the same result, every single time, whether that sequence comes from real clicks, a scripted replay like this one, or a test file that never renders a component at all. <code>useState<\/code> updater functions can be pure too, but they&#8217;re never captured as data the way an action object is \u2014 there&#8217;s nothing to &#8220;replay,&#8221; because there was never a recorded list of what happened, only a series of direct value changes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> understanding <em>why<\/em> reducers are considered more testable, not just being told that they are. <strong>Tip:<\/strong> this is also the exact mechanism behind tools like Redux DevTools&#8217; time-travel debugging \u2014 record the dispatched actions, and you can replay, rewind, or fast-forward through them against the reducer at any time, entirely independent of the UI that originally triggered them.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">10. Which One Should You Use?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Answer four honest questions about the state you&#8217;re actually building, and this small quiz tallies a recommendation.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/05\/10.html\" style=\"width:100%;height:420px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"Which One Should You Use Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const QUESTIONS = &#91;\n  'Is this more than two or three related values that always change together?',\n  'Does figuring out the next state need more than a one-line calculation?',\n  'Do several different event handlers update this same state?',\n  'Would you like to unit-test the state transitions on their own?'\n];\n\/\/ two or more \"yes\" answers \u2192 useReducer; otherwise \u2192 useState<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Fittingly, the quiz itself is built with <code>useReducer<\/code> \u2014 each answer is a dispatched action, and one function decides whether that action advances the question index or finalizes the result. That&#8217;s not a coincidence for effect; a multi-step quiz with a running tally and a distinct &#8220;done&#8221; state is exactly the shape of problem \u2014 several related pieces of state, several different sources of updates, no single field independently sufficient \u2014 that tips the scale toward a reducer, which is precisely what questions 1 and 3 above are asking about.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> a quick gut-check on any component where you&#8217;re genuinely unsure which hook fits. <strong>Tip:<\/strong> treat the result as a strong default, not a law \u2014 two &#8220;yes&#8221; answers is a reasonable line, but a component that&#8217;s simple today and clearly about to grow (a form you know will gain cross-field validation next sprint) is a fair reason to start with <code>useReducer<\/code> a little early.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Choosing between them, in practice<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Use useState when each value&#8217;s next state is a short, self-contained calculation.<\/strong> A toggle, a single counter, an independent form field \u2014 you don&#8217;t need to look at anything else to know what the next value should be.<\/li>\n\n\n\n<li><strong>Use useReducer when several pieces of state change together, or the same state can be reached from several different places.<\/strong> A cart, a history stack, a wizard, a state machine \u2014 you want one function that&#8217;s the single source of truth for what a given action actually does.<\/li>\n\n\n\n<li><strong>Reach for useReducer when you want a readable, testable log of what happened, not just the current value.<\/strong> Demos #3 and #9 show why: actions are data, and data can be logged, replayed, and unit-tested independently of any component.<\/li>\n\n\n\n<li><strong>Don&#8217;t reach for useReducer by default.<\/strong> Demo #7&#8217;s independent form fields are the common case where adding a reducer is pure ceremony \u2014 three unrelated <code>useState<\/code> calls are simpler to read and simpler to change than one reducer with three unrelated action types.<\/li>\n\n\n\n<li><strong>The two aren&#8217;t mutually exclusive within one component.<\/strong> It&#8217;s entirely normal for a component to hold one <code>useReducer<\/code> for its genuinely coupled state (say, a multi-step form&#8217;s step index and validation) alongside a plain <code>useState<\/code> for something unrelated (whether a tooltip is currently open).<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Frequently asked questions<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Is useReducer just Redux inside a component?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The pattern is the same \u2014 a pure <code>(state, action) =&gt; newState<\/code> function plus dispatched action objects \u2014 but useReducer is local to one component&#8217;s state by default and needs nothing installed. Redux exists to solve a different problem on top of that pattern: sharing one store across many components that aren&#8217;t related by props. If your state lives in and is only used by one component (or one that passes it down a short prop chain), useReducer alone is Redux&#8217;s core idea without Redux&#8217;s global-store machinery.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Is useReducer always the &#8220;more correct&#8221; choice for complex state?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No \u2014 complexity has to actually be about <em>coordination<\/em>, not just quantity. Demo #7&#8217;s form has three pieces of state, which sounds complex, but none of them coordinate with each other, so useState stays simpler. Demo #4&#8217;s two counters are the opposite: only two values, but they must always move together, and that coordination requirement is what useReducer actually solves.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I use useReducer for state that&#8217;s just one primitive value, like a number?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes \u2014 demo #9&#8217;s counter reducer holds a plain number, not an object. useReducer doesn&#8217;t require an object-shaped state; it requires that you want dispatched, named actions rather than direct value assignment. The state machine in demo #5 is the same idea with a string instead of a number.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Does useReducer replace useState entirely once I&#8217;ve learned it?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No, and demos #2 and #7 are the case for why: for state with no cross-field logic, useState is genuinely less code and easier to scan, not just &#8220;the beginner option.&#8221; Most real components end up with a mix \u2014 some fields as plain useState, one cluster of genuinely coupled state as a single useReducer \u2014 rather than standardizing on one hook everywhere.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How do I test a reducer without rendering a component?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Because a reducer is a plain function, you call it directly: <code>expect(reducer(initialState, { type: 'increment' })).toEqual({ count: 1 })<\/code>, no component, no DOM, no testing-library render step. Demo #8&#8217;s cart reducer and demo #9&#8217;s replay script both lean on this \u2014 the state logic is fully decoupled from anything React-specific.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Every demo above ships its full HTML, CSS and JS in the <strong>HTML \/ CSS \/ JS<\/strong> tabs in its own top bar. The fastest way to actually internalize the difference between these two hooks is to click <strong>Fork &amp; Edit<\/strong> on whichever demo felt least intuitive, change one action type, and watch exactly what breaks. Once you&#8217;ve got a version you like, save it to <a href=\"https:\/\/webdevpuneet.com\/ui-snippets\/mycode\/\">My Code<\/a> and it&#8217;s yours to keep, tweak further, or reuse in a real project.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Want to go deeper than a single embed? The <a href=\"https:\/\/webdevpuneet.com\/react-playground\/\">React Playground<\/a> is webdevpuneet&#8217;s full in-browser React sandbox \u2014 a real editor with instant preview, built for exactly this kind of edit-and-see learning, without the ten-tab constraint of a blog post.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Both hooks do the exact same job \u2014 they hold state and trigger a re-render when it changes. The real question isn&#8217;t &#8220;which one is more powerful,&#8221; it&#8217;s how you want to describe an update: with useState, the caller hands over the next value directly \u2014 setCount(count + 1) says &#8220;the count is now this.&#8221; [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":117,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_et_pb_use_builder":"","_et_pb_old_content":"","_et_gb_content_width":"","advanced_seo_description":"React useState vs useReducer explained with 10 live demos: action logs, a state machine, undo\/redo, a cart reducer and a quiz that tells you which hook to use.","jetpack_seo_html_title":"React useState vs useReducer: When to Use Each Hook","jetpack_seo_noindex":false,"jetpack_seo_schema_type":"","_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"React useState vs useReducer explained with 10 live demos: action logs, a state machine, undo\/redo, a cart reducer and a quiz that tells you which hook to use. #React #Webdev #Tutorials","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2},"jetpack_post_was_ever_published":false},"categories":[8,9],"tags":[],"class_list":["post-116","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-react","category-tutorials","et-has-post-format-content","et_post_format-et-post-format-standard"],"jetpack_publicize_connections":[],"jetpack_sharing_enabled":true,"jetpack_likes_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/phrh5V-1S","jetpack_featured_media_url":"https:\/\/i0.wp.com\/webdevpuneet.com\/blog\/wp-content\/uploads\/2026\/10\/react-usestate-vs-usereducer-1200x630-1.jpg?fit=1200%2C630&ssl=1","_links":{"self":[{"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/posts\/116","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/comments?post=116"}],"version-history":[{"count":1,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/posts\/116\/revisions"}],"predecessor-version":[{"id":118,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/posts\/116\/revisions\/118"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/media\/117"}],"wp:attachment":[{"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/media?parent=116"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/categories?post=116"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/tags?post=116"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}