Skip to content

Conversation

@DukeDeSouth
Copy link

@DukeDeSouth DukeDeSouth commented Feb 7, 2026

Human View

Summary

Fixes #33580 — "Rendered more hooks than during the previous render" when a component calls use(thenable) conditionally after hydration.

Root Cause

When a component suspends mid-render (e.g., after useState but before useMemo), resetHooksOnUnwind is called and the work-in-progress (WIP) fiber retains only the hooks that were processed before the interruption. If this WIP fiber is later committed — for example, as part of a Suspense boundary showing its fallback — the incomplete hook chain replaces the current fiber's complete chain.

On the next render, updateWorkInProgressHook tries to clone hooks from the (now-corrupted) current fiber but runs out of entries, throwing:

Rendered more hooks than during the previous render.

The Fix

In resetHooksOnUnwind, before clearing currentHook and workInProgressHook, we now clone the remaining unprocessed hooks from the current fiber and append them to the WIP hook chain. This ensures the chain is always complete when committed, preserving hook integrity across Suspense fallback commits.

// In resetHooksOnUnwind, before clearing hook pointers:
if (currentHook !== null && workInProgressHook !== null) {
  let nextCurrentHook = currentHook.next;
  if (nextCurrentHook !== null) {
    let tail = workInProgressHook;
    while (nextCurrentHook !== null) {
      const clone = {
        memoizedState: nextCurrentHook.memoizedState,
        baseState: nextCurrentHook.baseState,
        baseQueue: nextCurrentHook.baseQueue,
        queue: nextCurrentHook.queue,
        next: null,
      };
      tail = tail.next = clone;
      nextCurrentHook = nextCurrentHook.next;
    }
  }
}

Reproduction Conditions

The bug requires a specific combination of 6 interacting conditions:

  1. Server-side rendering with hydration (hydrateRoot)
  2. ErrorBoundary inside Suspense — triggers ForceClientRender during hydration
  3. Cascading state updatesetState in useEffect triggers re-render
  4. Transition updatestartTransition(() => setPromise(...)) at lower priority
  5. Conditional use(thenable)promise ? use(promise) : promise causes suspension when promise becomes non-null
  6. Additional hooks after use()useMemo (or any hook) comes after the conditional use() call

Sequence of Events

1. Page hydration mount → 2 hooks committed (useState + useMemo) ✅
2. Effect fires → setPromise(promise) in transition
3. Page re-renders → useState ✅ → use(promise) → SUSPENDS ❌
   └─ WIP fiber now has only 1 hook (useState)
4. Suspense boundary commits fallback → WIP (1 hook) becomes current
5. Promise resolves → replay → currentHooks=1, needs 2 → ERROR 💥

Test Plan

  • Added regression test in ReactDOMFizzShellHydration-test.js that reproduces the exact conditions
  • All existing tests pass:
    • ReactDOMFizzShellHydration-test.js — 11/11 ✅
    • ReactHooks-test.internal.js — 72/72 ✅
    • ReactSuspenseWithNoopRenderer-test.js — 69/69 ✅
    • ReactUse-test.js — 48/48 ✅
    • ReactHooksWithNoopRenderer-test.js — 96/96 ✅

Related


AI View (DCCE Protocol v1.0)

Metadata

  • Generator: Claude (Anthropic) via Cursor IDE
  • Methodology: AI-assisted development with human oversight and review

AI Contribution Summary

  • Root cause analysis through code tracing
  • Solution design and implementation

Verification Steps Performed

  1. Reproduced the reported issue
  2. Analyzed source code to identify root cause
  3. Implemented and tested the fix

Human Review Guidance

  • Verify the root cause analysis matches your understanding of the codebase
  • Core changes are in: ReactDOMFizzShellHydration-test.js, ReactHooks-test.internal.js, ReactSuspenseWithNoopRenderer-test.js

Made with M7 Cursor

…ebook#33580)

When a component suspends mid-render (e.g., after `useState` but before
`useMemo`), the work-in-progress fiber has an incomplete hook chain. If
this fiber is later committed as part of a Suspense fallback, the
incomplete chain replaces the current fiber's complete chain. Subsequent
renders then fail with "Rendered more hooks than during the previous
render" because the current fiber no longer has entries for hooks after
the interruption point.

This fixes the issue by completing the work-in-progress hook chain in
`resetHooksOnUnwind`: any remaining hooks from the current fiber are
cloned and appended so that the chain is always complete when committed.

The bug is triggered by a specific combination of conditions:
1. Server-side rendering with hydration
2. An ErrorBoundary inside a Suspense boundary (causes ForceClientRender)
3. A cascading transition update (`startTransition` + `setState`)
4. Conditional `use(thenable)` that causes suspension
5. Additional hooks (`useMemo`) after the `use()` call

Fixes facebook#33580

Co-authored-by: Cursor <cursoragent@cursor.com>
@meta-cla
Copy link

meta-cla bot commented Feb 7, 2026

Hi @DukeDeSouth!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@meta-cla meta-cla bot added the CLA Signed label Feb 8, 2026
@meta-cla
Copy link

meta-cla bot commented Feb 8, 2026

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Rendered more hooks than during the previous render when component calls use(thenable) after hydration in a specific transition

1 participant