[DevTools] Fix Profiler crash when recording why a memo() wrapper rendered #37819

Open
hoxyq opened 5:27pm on October 12, 2026 wants to merge 131 ฮ” into facebook/react main from
devtools-memo-change-descriptions

Website or app

https://stackblitz.com/edit/vitejs-vite-qfa8tyuo?file=src/App.tsx

Repro steps

React version

  • React: 19.3.0
  • React DOM: 19.3.0
  • React Developer Tools Chrome extension: 8.0.0
  • Extension revision: 1d34f91dfd

Reproduction

Important: open the StackBlitz preview in a separate browser tab. React DevTools cannot profile the embedded StackBlitz iframe.

Steps to reproduce

  1. Open the StackBlitz project above.
  2. Open its preview in a separate tab.
  3. Open React Developer Tools โ†’ Profiler.
  4. In Profiler settings, enable Record why each component rendered while profiling.
  5. Start a profiling recording.
  6. Type one character into the input.
  7. Stop the recording.

Actual behavior

React DevTools reports:

React instrumentation encountered an error:
Error: Unknown Fiber. Needs to be a function component to inspect hooks.
    at inspectHooksOfFiberImpl (installHook.js)
    at inspectHooksOfFiber (installHook.js)
    at inspectHooks (installHook.js)
    at recordProfilingDurations (installHook.js)
    at updateFiberRecursively (installHook.js)

The error is triggered by a controlled component implemented as memo(forwardRef(...)) when its parent re-renders and supplies a new inline onChange callback.

Typing changes the controlled value and re-renders the memoized forward-ref input with changed props. This triggers the DevTools instrumentation error.

Expected behavior

The Profiler should record render reasons without throwing an instrumentation error.

Minimal reproduction shape

const ControlledMemoInput = memo(
    forwardRef(function ControlledMemoInput(
        { onChange, value },
        forwardedRef
    ) {
        const context = useContext(InputContext);
        const inputRef = useRef(null);
        const isFilled = useMemo(() => value.length > 0, [value]);

        useEffect(() => {
            if (typeof forwardedRef === "function") {
                forwardedRef(inputRef.current);
            }
        }, [forwardedRef]);

        return (
            <input
                ref={inputRef}
                disabled={context.disabled}
                onChange={onChange}
                value={value}
                data-filled={isFilled}
            />
        );
    })
);

function App() {
    const [value, setValue] = useState("");
    const [renderRevision, setRenderRevision] = useState(0);

    useEffect(() => {
        setRenderRevision(1);
    }, []);

    return (
        <ControlledMemoInput
            value={value}
            onChange={(event) => setValue(event.target.value)}
        />
    );
}

Investigation

The failure appears to be inside React DevToolsโ€™ render-change inspection.

The profilerโ€™s change-description path handles MemoComponent fibers and calls inspectHooks, but inspectHooksOfFiberImpl rejects the outer memo fiber with:

Unknown Fiber. Needs to be a function component to inspect hooks.

The relevant component is a memo(forwardRef(...)) Fiber, not a plain function component, simple memo component, or plain forward-ref component.

Workaround

Disable Record why each component rendered while profiling. Normal profiling continues to work, but render-reason information is unavailable.

How often does this bug happen?

Every time

DevTools package (automated)

No response

DevTools version (automated)

No response

Error message (automated)

No response

Error call stack (automated)


Error component stack (automated)


GitHub query string (automated)


Diff Delta:
131
About 59 Diff Delta/hour
Classified as:  Bug Fix

hoxyq's Description of Work #37733 [DevTools Bug]: Profiler crashes for a controlled `memo(forwardRef(...))` component when recording render reasons

Summary

Fixes #37733.

With Record why each component rendered while profiling enabled, re-rendering a memo(forwardRef(...)) component with new props throws Unknown Fiber. Needs to be a function component to inspect hooks. from the commit hook. DevTools reports it as "React instrumentation encountered an error". memo(fn, compare) hits the same error.

This is a regression from #35123. getChangeDescription used to read fiber.memoizedState directly. That is null on a MemoComponent, so it reported hooks: null. Now it calls inspectHooks() on the previous and next fiber for every function-like tag, but inspectHooksOfFiber only accepts FunctionComponent, SimpleMemoComponent and ForwardRef. React creates a MemoComponent fiber for memo() of anything other than a plain function without a compare function. It is only a wrapper, and its child fiber owns the hooks. The existing test only covered a memo(Child, areEqual) that bails out, so this path never ran.

The fix only inspects hooks when both fibers have one of those tags, which is the same gate inspectElement uses (usesHooks). It also requires the renderer to inject currentDispatcherRef, which shipped with hooks in 16.8. React 16.5โ€“16.7 still support profiling, but inspectHooksOfFiber throws there for every function component: those fibers only have firstContextDependency, so it fails with "Unsupported React version". It also throws for forwardRef on 16.5, where the tag is 13. In all other cases the change description reports hooks: null, as it did before #35123, and the frontend already handles that. The inner forwardRef or function fiber still reports its own hook changes.

Not addressed here: since #35123, the commit path re-runs component render functions. A component that throws when re-rendered with the previous fiber's props also crashes the instrumentation, with ReactDebugToolsRenderError. One example is a component that reads an item that has since been removed from an external store. That is a separate issue.

How did you test this change?


  • Added two tests to profilerChangeDescriptions-test.js:



    • memo(forwardRef()), in the issue's shape: a controlled input with an inline onChange. It also checks that a state update inside the forwardRef is still reported (didHooksChange: true, hooks: [0]) while the memo wrapper bails out.


    • memo() with a custom compare function.



Both fail on main with "Unknown Fiber. Needs to be a function component to inspect hooks." and pass with this change.
- yarn test --build --project=devtools: 597 pass. The only failure is inspectedElement-test "inspecting nested renderers should not throw (createRoot)", which fails the same way on main locally (react-art's canvas setTransform is null in jsdom). The source and www Jest configs exclude the DevTools packages, so yarn test and yarn test-www match no tests.
- Flow with the dom-browser config, which covers react-devtools-shared: no errors. yarn prettier and yarn linc: pass.
- I didn't run the 16.5 regression lane locally. The pre-16.8 gate is based on reading the 16.5โ€“16.8 reconciler source.

2 total changed files
Loading changes...
Loading changes...
You've reached the end of this PR review
The diff well is dry.