From 7dfea9656358f425c3a4ce62e26a529e3474f697 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Wed, 6 Mar 2024 18:07:46 +0100 Subject: [PATCH 1/2] Current behavior for returning incorrect deferred value in debug-tools dispatcher --- .../ReactHooksInspectionIntegration-test.js | 244 ++++++++++++++++++ 1 file changed, 244 insertions(+) diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index 97110f682d572..e741a84063d43 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -1206,6 +1206,250 @@ describe('ReactHooksInspectionIntegration', () => { `); }); + it('should return the deferred value', async () => { + let unsuspend; + function Lazy() { + return 'Lazy'; + } + const Suspender = React.lazy( + () => + new Promise(resolve => { + unsuspend = () => resolve({default: Lazy}); + }), + ); + const Context = React.createContext('default'); + let setShow; + function Foo(props) { + const [show, _setShow] = React.useState(false); + const deferredShow = React.useDeferredValue(show); + const isPending = show !== deferredShow; + const contextDisplay = isPending ? React.use(Context) : ''; + React.useMemo(() => 'hello', []); + React.useMemo(() => 'not used', []); + + // Otherwise we capture the version from the react-debug-tools dispatcher. + if (setShow === undefined) { + setShow = _setShow; + } + + return ( + + Context: {contextDisplay}, {isPending ? 'Pending' : 'Nothing Pending'} + {deferredShow ? [', ', ] : null} + + ); + } + const renderer = await act(() => { + return ReactTestRenderer.create( + + + , + {isConcurrent: true}, + ); + }); + let childFiber = renderer.root.findByType(Foo)._currentFiber(); + let tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + expect(renderer).toMatchRenderedOutput('Context: , Nothing Pending'); + expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(` + [ + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "Foo", + "lineNumber": 0, + }, + "id": 0, + "isStateEditable": true, + "name": "State", + "subHooks": [], + "value": false, + }, + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "Foo", + "lineNumber": 0, + }, + "id": 1, + "isStateEditable": false, + "name": "DeferredValue", + "subHooks": [], + "value": false, + }, + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "Foo", + "lineNumber": 0, + }, + "id": 2, + "isStateEditable": false, + "name": "Memo", + "subHooks": [], + "value": "hello", + }, + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "Foo", + "lineNumber": 0, + }, + "id": 3, + "isStateEditable": false, + "name": "Memo", + "subHooks": [], + "value": "not used", + }, + ] + `); + + await act(() => { + setShow(true); + }); + + expect(renderer).toMatchRenderedOutput('Context: provided, Pending'); + childFiber = renderer.root.findByType(Foo)._currentFiber(); + tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(` + [ + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "Foo", + "lineNumber": 0, + }, + "id": 0, + "isStateEditable": true, + "name": "State", + "subHooks": [], + "value": true, + }, + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "Foo", + "lineNumber": 0, + }, + "id": 1, + "isStateEditable": false, + "name": "DeferredValue", + "subHooks": [], + "value": false, + }, + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "Foo", + "lineNumber": 0, + }, + "id": 2, + "isStateEditable": false, + "name": "Memo", + "subHooks": [], + "value": "hello", + }, + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "Foo", + "lineNumber": 0, + }, + "id": 3, + "isStateEditable": false, + "name": "Memo", + "subHooks": [], + "value": "not used", + }, + ] + `); + + await act(() => { + unsuspend(); + }); + + expect(renderer).toMatchRenderedOutput( + 'Context: , Nothing Pending, Lazy', + ); + childFiber = renderer.root.findByType(Foo)._currentFiber(); + tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(` + [ + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "Foo", + "lineNumber": 0, + }, + "id": 0, + "isStateEditable": true, + "name": "State", + "subHooks": [], + "value": true, + }, + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "Foo", + "lineNumber": 0, + }, + "id": 1, + "isStateEditable": false, + "name": "DeferredValue", + "subHooks": [], + "value": true, + }, + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "Foo", + "lineNumber": 0, + }, + "id": 2, + "isStateEditable": false, + "name": "Memo", + "subHooks": [], + "value": "hello", + }, + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "Foo", + "lineNumber": 0, + }, + "id": 3, + "isStateEditable": false, + "name": "Memo", + "subHooks": [], + "value": "not used", + }, + ] + `); + }); + it('should support useId hook', () => { function Foo(props) { const id = React.useId(); From f94d6f746c5b62554fcf8bd5c679a85de8ee5128 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Wed, 6 Mar 2024 18:18:35 +0100 Subject: [PATCH 2/2] Devtools: Ensure component control flow is consistent with commit when using `useDeferredValue` --- packages/react-debug-tools/src/ReactDebugHooks.js | 5 +++-- .../ReactHooksInspectionIntegration-test.js | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 20e3d89e1bf64..8e1809435d724 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -444,14 +444,15 @@ function useTransition(): [ function useDeferredValue(value: T, initialValue?: T): T { const hook = nextHook(); + const prevValue = hook !== null ? hook.memoizedState : value; hookLog.push({ displayName: null, primitive: 'DeferredValue', stackError: new Error(), - value: hook !== null ? hook.memoizedState : value, + value: prevValue, debugInfo: null, }); - return value; + return prevValue; } function useId(): string { diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index e741a84063d43..61eefa844aece 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -1348,6 +1348,20 @@ describe('ReactHooksInspectionIntegration', () => { "subHooks": [], "value": false, }, + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "Foo", + "lineNumber": 0, + }, + "id": null, + "isStateEditable": false, + "name": "Context", + "subHooks": [], + "value": "provided", + }, { "debugInfo": null, "hookSource": {