Fixing React useReducer State Resets That Lose Updates on Re-mount

June 06, 2026 7 min read 54 views
Abstract flat illustration of floating geometric nodes representing component state lifecycle, against a soft blue gradient background

You dispatch an action, the UI updates, and then the component unmounts for a split second β€” a route transition, a conditional render, a Suspense boundary β€” and when it comes back, the state is gone. Your reducer is fine. Your dispatch calls are fine. The problem is that React treats every new mount as a fresh start.

This article explains exactly what causes useReducer state to reset on re-mount and walks through several concrete fixes ranked by complexity.

What you'll learn

  • Why useReducer state is tied to component instance lifetime, not component identity
  • How to diagnose whether a reset is caused by unmounting or something else
  • Four practical strategies to persist state across re-mounts
  • When each strategy is the right tool for the job
  • Common pitfalls that make the problem worse

Prerequisites

You should be comfortable with React hooks, understand the basics of useReducer, and have a rough mental model of how React's reconciler works. No external libraries are required for most fixes, though one section touches on Zustand as an option.

Why useReducer Resets on Re-mount

React stores hook state in a "fiber" β€” an internal node in the component tree. When a component unmounts, React discards that fiber along with every hook value attached to it. When the component mounts again, React creates a brand-new fiber and calls useReducer from scratch, passing your initialState or calling your init function again.

This is intentional behavior. React does not keep a cache of hook state for components that are no longer in the tree. The lifecycle is strict: mount β†’ state lives β†’ unmount β†’ state dies.

The confusion arises because component identity (same function, same position in the tree) feels like it should imply continuity. It doesn't. Only an uninterrupted presence in the tree guarantees that state survives.

Diagnosing the Problem

Before reaching for a fix, confirm that unmounting is actually what's happening. There are three other reasons useReducer can appear to reset:

  • Stale closure in a dispatch call β€” a callback captured an old version of dispatch or state.
  • Key prop change β€” changing the key prop on a component forces React to unmount and remount it immediately.
  • Reducer returning the initial state accidentally β€” a missing default branch in your switch statement silently returns undefined, which React replaces with the initial state.

Add a useEffect with an empty dependency array and log a message on both mount and cleanup. If you see mount β†’ unmount β†’ mount in quick succession, the component is definitely cycling.

useEffect(() => {
  console.log('mounted');
  return () => console.log('unmounted');
}, []);

React DevTools Profiler will also show you which components are being committed in each render pass. That's often faster than adding logs.

Fix 1: Stabilize the Key Prop

If the re-mount is caused by a changing key prop, the fix is simply to stop changing the key. This sounds obvious, but it's surprisingly common to use an index or a derived value as a key when it shouldn't be.

// Bad β€” index changes when list order changes, forcing remounts
{items.map((item, index) => (
  <ItemEditor key={index} item={item} />
))}

// Good β€” stable identity
{items.map((item) => (
  <ItemEditor key={item.id} item={item} />
))}

If you are intentionally using key to reset state (a legitimate pattern), then the state reset is by design. Don't fight it β€” instead rethink whether a key reset is the correct reset mechanism for your use case.

Fix 2: Lift State to a Parent That Stays Mounted

The cleanest architectural fix is to move the useReducer call up to a parent component that never unmounts during the scenario you care about. The child component becomes stateless from the reducer's perspective β€” it just receives props and dispatches actions upward.

// Parent β€” always mounted
function EditorContainer() {
  const [state, dispatch] = useReducer(editorReducer, initialState);

  return (
    <Routes>
      <Route path="/edit" element={<Editor state={state} dispatch={dispatch} />} />
      <Route path="/preview" element={<Preview state={state} />} />
    </Routes>
  );
}

// Child β€” can unmount freely
function Editor({ state, dispatch }) {
  return (
    <input
      value={state.title}
      onChange={(e) => dispatch({ type: 'SET_TITLE', payload: e.target.value })}
    />
  );
}

This pattern works well when the state is genuinely shared between routes or siblings. It breaks down if the state is truly private to the child and lifting it creates awkward prop drilling.

Fix 3: Persist State with useRef Between Mounts

When you can't lift state and you want a zero-dependency solution, store the last known state in a useRef attached to a parent component, then pass it as the initialState when the child re-mounts.

function EditorContainer() {
  const savedState = useRef(initialEditorState);

  return <Editor savedState={savedState} />;
}

function Editor({ savedState }) {
  const [state, dispatch] = useReducer(editorReducer, savedState.current);

  // Sync the latest state back to the ref on every render
  useEffect(() => {
    savedState.current = state;
  });

  return <input value={state.title} onChange={/* ... */} />;
}

The key detail: the useEffect with no dependency array runs after every render, keeping savedState.current up to date. When the child unmounts and remounts, useReducer initializes from the ref's current value instead of the hardcoded initial state.

One caveat: useRef is mutable and does not trigger re-renders, so this is a write-and-forget pattern. That's fine here because you're using it as a snapshot buffer, not reactive state.

Fix 4: Move State Outside React with a Store

For state that needs to survive across unmounts reliably and be accessible from multiple components, move it out of the component tree entirely. You can use React Context combined with useReducer at a high level, or reach for a lightweight store like Zustand.

The Context approach keeps things inside React's model:

const EditorContext = React.createContext(null);

export function EditorProvider({ children }) {
  const [state, dispatch] = useReducer(editorReducer, initialState);
  return (
    <EditorContext.Provider value={{ state, dispatch }}>
      {children}
    </EditorContext.Provider>
  );
}

export function useEditor() {
  return React.useContext(EditorContext);
}

Place EditorProvider above any route or conditional that could cause the consuming component to unmount. The state now lives in the Provider's fiber, which stays mounted as long as the Provider does.

If even the Provider can unmount, Zustand stores state entirely outside the React tree and survives any mount/unmount cycle without extra plumbing. It's a pragmatic choice when the Context approach feels like too many moving parts.

Fix 5: Use React's Built-in Activity (Experimental)

React's experimental <Activity> component (previously discussed under the offscreen API) is designed exactly for this scenario. It lets React keep a subtree's state alive while hiding it from the screen, rather than unmounting it.

// Conceptual β€” API is still experimental as of recent React canary builds
import { unstable_Activity as Activity } from 'react';

<Activity mode={isVisible ? 'visible' : 'hidden'}>
  <Editor />
</Activity>

When mode is 'hidden', the component stays mounted in the tree but is not painted. Its hook state, including your useReducer, is fully preserved. This is the most direct solution but it's not production-stable across all React versions yet. Keep an eye on the React changelog before adopting it in a critical path.

Common Pitfalls

Passing a new object reference as initialState every render

If you write useReducer(reducer, { count: 0 }) inside a component, JavaScript creates a new object on every render call. React only reads initialState on the first mount, so this doesn't cause repeated resets β€” but it does mean the initial state is re-evaluated as an expression every render, which is wasteful. Define initialState outside the component or use the lazy initializer form: useReducer(reducer, arg, initFn).

Forgetting the default case in your reducer

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    // Missing default β€” returns undefined on unknown actions
  }
}

Always add default: return state;. Without it, any unknown action type silently corrupts your state, which can look identical to a reset if the initial state and undefined produce similar-looking renders.

Relying on Strict Mode double-invocation behavior

In development with React.StrictMode, React intentionally mounts, unmounts, and remounts every component to help you detect side-effect bugs. If your state loss only reproduces in development and not production, this is likely why. It's not a bug to ignore β€” it's a signal that your component isn't handling the mount/unmount cycle cleanly.

Losing state during Suspense fallback transitions

When a component is inside a <Suspense> boundary and the boundary falls back, children may unmount depending on the React version and how the boundary is structured. Lifting state above the Suspense boundary is the safest fix here.

Choosing the Right Fix

ScenarioBest fix
Key prop is changing unnecessarilyStabilize the key
State is shared across routes or siblingsLift state to a parent
State is private, parent is availableuseRef snapshot in parent
State needed across many componentsContext + useReducer provider
Complex global state, multiple consumersExternal store (Zustand etc.)
Hiding UI without unmounting (future)Activity component

Wrapping Up

State loss on re-mount is one of those bugs that feels mysterious until you understand React's fiber lifetime model. Once it clicks, the fix is usually straightforward.

Here are your concrete next steps:

  1. Add mount/unmount logs to confirm the component is actually cycling before you change any code.
  2. Check your key props first β€” it's the most common cause and the easiest fix.
  3. If the component legitimately unmounts, decide whether to lift state or use a ref-based snapshot based on whether the state is shared or private.
  4. Add a default: return state; branch to every reducer you own if it doesn't already have one.
  5. Watch the React changelog for the Activity API stabilizing β€” it will become the standard solution for keep-alive patterns.

πŸ“€ Share this article

Sign in to save

Comments (0)

No comments yet. Be the first!

Leave a Comment

Sign in to comment with your profile.

πŸ“¬ Weekly Newsletter

Stay ahead of the curve

Get the best programming tutorials, data analytics tips, and tool reviews delivered to your inbox every week.

No spam. Unsubscribe anytime.