diff --git a/packages/react-devtools-shared/src/__tests__/profilerStore-test.js b/packages/react-devtools-shared/src/__tests__/profilerStore-test.js index c8384b2fa42..184a141c323 100644 --- a/packages/react-devtools-shared/src/__tests__/profilerStore-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilerStore-test.js @@ -233,4 +233,87 @@ describe('ProfilerStore', () => { utils.act(() => render()); utils.act(() => store.profilerStore.startProfiling()); }); + + // @reactVersion >= 18.2 + // Verifies that cached JSX children (via useMemoCache) are not reported as rendered. + it('should not report cached/memoized children as rendered when parent re-renders', () => { + const Scheduler = require('scheduler'); + const useMemoCache = require('react/compiler-runtime').c; + let forceTextUpdate; + + function Child({count}: {count: number}) { + Scheduler.unstable_advanceTime(10); + return
Count: {count}
; + } + + function Parent() { + const $ = useMemoCache(4); + + const [count] = React.useState(0); + const [text, setText] = React.useState(''); + + forceTextUpdate = setText; + + Scheduler.unstable_advanceTime(5); + + let t0; + if ($[0] !== count) { + t0 = ( +
+ +
+ ); + $[0] = count; + $[1] = t0; + } else { + t0 = $[1]; + } + + let t1; + if ($[2] !== text || $[3] !== t0) { + t1 = ( +
+ {text} + {t0} +
+ ); + $[2] = text; + $[3] = t0; + } else { + t1 = $[3]; + } + + return t1; + } + + utils.act(() => render()); + utils.act(() => store.profilerStore.startProfiling()); + + // Updating text should not cause Child to be reported as rendered, + // because the cached child JSX (t0) stays the same. + utils.act(() => forceTextUpdate('a')); + + utils.act(() => store.profilerStore.stopProfiling()); + + const rootID = store.roots[0]; + const data = store.profilerStore.getDataForRoot(rootID); + + expect(data.commitData).toHaveLength(1); + + const commit = data.commitData[0]; + const renderedFiberIds = Array.from(commit.fiberActualDurations.keys()); + + let childElementId = null; + for (let i = 0; i < store.numElements; i++) { + const element = store.getElementAtIndex(i); + if (element?.displayName === 'Child') { + childElementId = element.id; + break; + } + } + + expect(childElementId).not.toBeNull(); + // Child should NOT be in fiberActualDurations because the fiber was never visited. + expect(renderedFiberIds).not.toContain(childElementId); + }); }); diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 289bad6f329..c6ada5809b8 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -2086,6 +2086,12 @@ export function attach( } function didFiberRender(prevFiber: Fiber, nextFiber: Fiber): boolean { + // Note: prevFiber === nextFiber means the fiber was never visited this render. + // Any PerformedWork flag is stale because the parent bailed out entirely. + if (prevFiber === nextFiber) { + return false; + } + switch (nextFiber.tag) { case ClassComponent: case FunctionComponent: