nextjs - 💡(How to fix) Fix State reset during useLayoutEffect context update in React 19 [2 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vercel/next.js#89050Fetched 2026-04-08 02:03:20
View on GitHub
Comments
2
Participants
3
Timeline
5
Reactions
0
Timeline (top)
commented ×2closed ×1labeled ×1locked ×1

Root Cause

After clicking EDIT (which sets a localStorage flag), refreshing the page works because:

  1. Component mounts fresh with no previous state
  2. useState initializer reads localStorage and returns true
  3. No state transition occurs - component starts in edit mode
  4. useLayoutEffect runs but doesn't interfere with an already-settled state

Fix Action

Workaround

Changed useLayoutEffect to useEffect and wrapped context updates in startTransition:

import { useEffect, startTransition } from 'react';

export function useSetRightRail(content: ReactNode, updateKey?: unknown) {
  const context = useContext(RightRailContext);

  // Use useEffect (not useLayoutEffect) to defer the context update
  useEffect(() => {
    if (context?.setContent) {
      startTransition(() => {
        context.setContent(content);
      });
    }
    return () => {
      startTransition(() => {
        context?.setContent(null);
      });
    };
  }, [context, content, updateKey]);
}

Additionally, for our use case, we had to implement a page reload strategy:

const startEditing = useCallback(() => {
  localStorage.setItem('editing-flag', 'true');
  window.location.reload(); // Force fresh mount
}, []);

Code Example

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.3.0

Binaries:
  Node: 22.x
  npm: 10.x

Relevant Packages:
  next: 16.1.1-canary.4
  react: 19.0.0
  react-dom: 19.0.0

Turbopack: Enabled

---

// contexts/right-rail-context.tsx
'use client';

import { createContext, useContext, ReactNode, useCallback, useLayoutEffect, useState } from 'react';

const RightRailContext = createContext<{ content: ReactNode; setContent: (c: ReactNode) => void } | null>(null);

export function RightRailProvider({ children }: { children: ReactNode }) {
  const [content, setContent] = useState<ReactNode>(null);
  return (
    <RightRailContext.Provider value={{ content, setContent }}>
      {children}
    </RightRailContext.Provider>
  );
}

// This hook causes the bug
export function useSetRightRail(content: ReactNode, updateKey?: unknown) {
  const context = useContext(RightRailContext);

  // Using useLayoutEffect to update context triggers the bug
  useLayoutEffect(() => {
    context?.setContent(content);
    return () => context?.setContent(null);
  }, [context, content, updateKey]);
}

---

// components/editor.tsx
'use client';

import { useState } from 'react';
import { useSetRightRail } from '@/contexts/right-rail-context';

export function Editor() {
  const [isEditing, setIsEditing] = useState(false);

  // This useLayoutEffect update interferes with the state transition
  useSetRightRail(
    isEditing ? <div>EDIT MODE</div> : null,
    isEditing // updateKey
  );

  const startEditing = () => {
    console.log('Before setState:', isEditing); // false
    setIsEditing(true);
    console.log('After setState:', isEditing); // still false (expected, async)
  };

  // On first click, this logs true briefly, then false
  console.log('Render with isEditing:', isEditing);

  return (
    <div>
      {isEditing ? (
        <form>Edit Form Here</form>
      ) : (
        <button onClick={startEditing}>EDIT</button>
      )}
    </div>
  );
}

---

Before setState: false
Render with isEditing: true    // Good!
Render with isEditing: false   // BAD - state reset!
Render with isEditing: false   // Stuck in view mode

---

import { useEffect, startTransition } from 'react';

export function useSetRightRail(content: ReactNode, updateKey?: unknown) {
  const context = useContext(RightRailContext);

  // Use useEffect (not useLayoutEffect) to defer the context update
  useEffect(() => {
    if (context?.setContent) {
      startTransition(() => {
        context.setContent(content);
      });
    }
    return () => {
      startTransition(() => {
        context?.setContent(null);
      });
    };
  }, [context, content, updateKey]);
}

---

const startEditing = useCallback(() => {
  localStorage.setItem('editing-flag', 'true');
  window.location.reload(); // Force fresh mount
}, []);
RAW_BUFFERClick to expand / collapse

Bug Report: State Reset During useLayoutEffect Context Update in React 19

Verify canance and latest version

  • I verified that the issue exists in the latest Next.js canary release
  • I verified this issue is not a duplicate

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.3.0

Binaries:
  Node: 22.x
  npm: 10.x

Relevant Packages:
  next: 16.1.1-canary.4
  react: 19.0.0
  react-dom: 19.0.0

Turbopack: Enabled

Which area(s) are affected?

  • App Router
  • Turbopack

Describe the Bug

When a component uses useLayoutEffect to update React Context state (via a custom hook), and another component in the same render tree triggers a state change, the state appears to "reset" during the render cascade. The context receives the correct value, but the component's local state reverts to its previous value.

Symptoms

  1. User clicks a button that calls setState(true)
  2. A useLayoutEffect in the same component updates context state
  3. The context correctly shows the new state
  4. BUT the component's local state appears to reset to false
  5. Page refresh works correctly - if you refresh while the localStorage flag is set, the component mounts with the correct state

Expected Behavior

When setIsEditing(true) is called:

  1. isEditing should become true and stay true
  2. The component should re-render with isEditing: true
  3. The edit form should appear

Actual Behavior

When setIsEditing(true) is called:

  1. isEditing briefly becomes true
  2. A context update triggers during useLayoutEffect
  3. isEditing appears to reset to false during the render cascade
  4. The component renders with isEditing: false (view mode)
  5. The context shows the correct state (edit mode in right rail)

Link to reproduction

Minimal reproduction pattern:

// contexts/right-rail-context.tsx
'use client';

import { createContext, useContext, ReactNode, useCallback, useLayoutEffect, useState } from 'react';

const RightRailContext = createContext<{ content: ReactNode; setContent: (c: ReactNode) => void } | null>(null);

export function RightRailProvider({ children }: { children: ReactNode }) {
  const [content, setContent] = useState<ReactNode>(null);
  return (
    <RightRailContext.Provider value={{ content, setContent }}>
      {children}
    </RightRailContext.Provider>
  );
}

// This hook causes the bug
export function useSetRightRail(content: ReactNode, updateKey?: unknown) {
  const context = useContext(RightRailContext);

  // Using useLayoutEffect to update context triggers the bug
  useLayoutEffect(() => {
    context?.setContent(content);
    return () => context?.setContent(null);
  }, [context, content, updateKey]);
}
// components/editor.tsx
'use client';

import { useState } from 'react';
import { useSetRightRail } from '@/contexts/right-rail-context';

export function Editor() {
  const [isEditing, setIsEditing] = useState(false);

  // This useLayoutEffect update interferes with the state transition
  useSetRightRail(
    isEditing ? <div>EDIT MODE</div> : null,
    isEditing // updateKey
  );

  const startEditing = () => {
    console.log('Before setState:', isEditing); // false
    setIsEditing(true);
    console.log('After setState:', isEditing); // still false (expected, async)
  };

  // On first click, this logs true briefly, then false
  console.log('Render with isEditing:', isEditing);

  return (
    <div>
      {isEditing ? (
        <form>Edit Form Here</form>
      ) : (
        <button onClick={startEditing}>EDIT</button>
      )}
    </div>
  );
}

Console output on first click

Before setState: false
Render with isEditing: true    // Good!
Render with isEditing: false   // BAD - state reset!
Render with isEditing: false   // Stuck in view mode

Why page refresh works

After clicking EDIT (which sets a localStorage flag), refreshing the page works because:

  1. Component mounts fresh with no previous state
  2. useState initializer reads localStorage and returns true
  3. No state transition occurs - component starts in edit mode
  4. useLayoutEffect runs but doesn't interfere with an already-settled state

Workaround

Changed useLayoutEffect to useEffect and wrapped context updates in startTransition:

import { useEffect, startTransition } from 'react';

export function useSetRightRail(content: ReactNode, updateKey?: unknown) {
  const context = useContext(RightRailContext);

  // Use useEffect (not useLayoutEffect) to defer the context update
  useEffect(() => {
    if (context?.setContent) {
      startTransition(() => {
        context.setContent(content);
      });
    }
    return () => {
      startTransition(() => {
        context?.setContent(null);
      });
    };
  }, [context, content, updateKey]);
}

Additionally, for our use case, we had to implement a page reload strategy:

const startEditing = useCallback(() => {
  localStorage.setItem('editing-flag', 'true');
  window.location.reload(); // Force fresh mount
}, []);

Additional Context

  • This bug does not occur in React 18
  • This bug occurs with Turbopack enabled
  • Likely related to React 19's concurrent rendering changes
  • The issue seems specific to useLayoutEffect triggering context state updates during a state transition
  • Similar to but distinct from #19103 (StrictMode state reset)

Related Issues

extent analysis

TL;DR

The most likely fix for the state reset issue during useLayoutEffect context update in React 19 is to replace useLayoutEffect with useEffect and wrap context updates in startTransition.

Guidance

  • Identify instances of useLayoutEffect that update context state and consider replacing them with useEffect to defer context updates.
  • Wrap context updates in startTransition to ensure they are handled as non-urgent, allowing the component to settle before updating the context.
  • If the issue persists, consider implementing a page reload strategy, such as setting a flag in local storage and reloading the page to force a fresh mount.
  • Be aware that this issue may be related to React 19's concurrent rendering changes and Turbopack enabled.

Example

import { useEffect, startTransition } from 'react';

export function useSetRightRail(content: ReactNode, updateKey?: unknown) {
  const context = useContext(RightRailContext);

  useEffect(() => {
    if (context?.setContent) {
      startTransition(() => {
        context.setContent(content);
      });
    }
    return () => {
      startTransition(() => {
        context?.setContent(null);
      });
    };
  }, [context, content, updateKey]);
}

Notes

This fix may not apply to all scenarios, and further investigation may be necessary to determine the root cause of the issue. The provided workaround has been successful in resolving the issue for the reported use case.

Recommendation

Apply the workaround by replacing useLayoutEffect with useEffect and wrapping context updates in startTransition, as this has been shown to resolve the issue in the reported use case.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING