diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 1a0c34e306d3f..8cca30df51c21 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -20,6 +20,7 @@ import type { Fiber, Dispatcher as DispatcherType, } from 'react-reconciler/src/ReactInternalTypes'; +import type {TransitionStatus} from 'react-reconciler/src/ReactFiberConfig'; import ErrorStackParser from 'error-stack-parser'; import assign from 'shared/assign'; @@ -124,6 +125,11 @@ function getPrimitiveStackCache(): Map> { ); } catch (x) {} } + + if (typeof Dispatcher.useHostTransitionStatus === 'function') { + // This type check is for Flow only. + Dispatcher.useHostTransitionStatus(); + } } finally { readHookLog = hookLog; hookLog = []; @@ -597,6 +603,26 @@ function useFormState( return [state, (payload: P) => {}]; } +function useHostTransitionStatus(): TransitionStatus { + const status = readContext( + // $FlowFixMe[prop-missing] `readContext` only needs _currentValue + ({ + // $FlowFixMe[incompatible-cast] TODO: Incorrect bottom value without access to Fiber config. + _currentValue: null, + }: ReactContext), + ); + + hookLog.push({ + displayName: null, + primitive: 'HostTransitionStatus', + stackError: new Error(), + value: status, + debugInfo: null, + }); + + return status; +} + const Dispatcher: DispatcherType = { use, readContext, @@ -619,6 +645,7 @@ const Dispatcher: DispatcherType = { useDeferredValue, useId, useFormState, + useHostTransitionStatus, }; // create a proxy to throw a custom error @@ -871,7 +898,8 @@ function buildTree( primitive === 'Context (use)' || primitive === 'DebugValue' || primitive === 'Promise' || - primitive === 'Unresolved' + primitive === 'Unresolved' || + primitive === 'HostTransitionStatus' ? null : nativeHookID++; diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegrationDOM-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegrationDOM-test.js new file mode 100644 index 0000000000000..724895be7fc74 --- /dev/null +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegrationDOM-test.js @@ -0,0 +1,185 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + * @jest-environment jsdom + */ + +'use strict'; + +let React; +let ReactDOM; +let ReactDOMClient; +let ReactDebugTools; +let act; + +function normalizeSourceLoc(tree) { + tree.forEach(node => { + if (node.hookSource) { + node.hookSource.fileName = '**'; + node.hookSource.lineNumber = 0; + node.hookSource.columnNumber = 0; + } + normalizeSourceLoc(node.subHooks); + }); + return tree; +} + +describe('ReactHooksInspectionIntegration', () => { + beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); + act = require('internal-test-utils').act; + ReactDebugTools = require('react-debug-tools'); + }); + + // @gate enableFormActions && enableAsyncActions + it('should support useFormStatus hook', async () => { + function FormStatus() { + const status = ReactDOM.useFormStatus(); + React.useMemo(() => 'memo', []); + React.useMemo(() => 'not used', []); + + return JSON.stringify(status); + } + + const treeWithoutFiber = ReactDebugTools.inspectHooks(FormStatus); + expect(normalizeSourceLoc(treeWithoutFiber)).toEqual([ + { + debugInfo: null, + hookSource: { + columnNumber: 0, + fileName: '**', + functionName: 'FormStatus', + lineNumber: 0, + }, + id: null, + isStateEditable: false, + name: '.useFormStatus', + subHooks: [ + { + debugInfo: null, + hookSource: { + columnNumber: 0, + fileName: '**', + functionName: 'Object.useFormStatus', + lineNumber: 0, + }, + id: null, + isStateEditable: false, + name: 'HostTransitionStatus', + subHooks: [], + value: null, + }, + ], + value: null, + }, + { + debugInfo: null, + hookSource: { + columnNumber: 0, + fileName: '**', + functionName: 'FormStatus', + lineNumber: 0, + }, + id: 0, + isStateEditable: false, + name: 'Memo', + subHooks: [], + value: 'memo', + }, + { + debugInfo: null, + hookSource: { + columnNumber: 0, + fileName: '**', + functionName: 'FormStatus', + lineNumber: 0, + }, + id: 1, + isStateEditable: false, + name: 'Memo', + subHooks: [], + value: 'not used', + }, + ]); + + const root = ReactDOMClient.createRoot(document.createElement('div')); + + await act(() => { + root.render( +
+ + , + ); + }); + + // Implementation detail. Feel free to adjust the position of the Fiber in the tree. + const formStatusFiber = root._internalRoot.current.child.child; + const treeWithFiber = ReactDebugTools.inspectHooksOfFiber(formStatusFiber); + expect(normalizeSourceLoc(treeWithFiber)).toEqual([ + { + debugInfo: null, + hookSource: { + columnNumber: 0, + fileName: '**', + functionName: 'FormStatus', + lineNumber: 0, + }, + id: null, + isStateEditable: false, + name: '.useFormStatus', + subHooks: [ + { + debugInfo: null, + hookSource: { + columnNumber: 0, + fileName: '**', + functionName: 'Object.useFormStatus', + lineNumber: 0, + }, + id: null, + isStateEditable: false, + name: 'HostTransitionStatus', + subHooks: [], + value: null, + }, + ], + value: null, + }, + { + debugInfo: null, + hookSource: { + columnNumber: 0, + fileName: '**', + functionName: 'FormStatus', + lineNumber: 0, + }, + id: 0, + isStateEditable: false, + name: 'Memo', + subHooks: [], + value: 'memo', + }, + { + debugInfo: null, + hookSource: { + columnNumber: 0, + fileName: '**', + functionName: 'FormStatus', + lineNumber: 0, + }, + id: 1, + isStateEditable: false, + name: 'Memo', + subHooks: [], + value: 'not used', + }, + ]); + }); +}); diff --git a/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js b/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js index 2a845ec12e4e9..18a95f96ab75d 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js @@ -21,7 +21,7 @@ import { useState, use, } from 'react'; -import {useFormState} from 'react-dom'; +import {useFormState, useFormStatus} from 'react-dom'; const object = { string: 'abc', @@ -136,6 +136,12 @@ function incrementWithDelay(previousState: number, formData: FormData) { }); } +function FormStatus() { + const status = useFormStatus(); + + return
{JSON.stringify(status)}
; +} + function Forms() { const [state, formAction] = useFormState(incrementWithDelay, 0); return ( @@ -156,6 +162,7 @@ function Forms() { + ); } diff --git a/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js b/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js index 492f2065edd77..b1b4e0afb3f2f 100644 --- a/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js +++ b/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js @@ -72,7 +72,11 @@ export function useFormStatus(): FormStatus { } else { const dispatcher = resolveDispatcher(); // $FlowFixMe[not-a-function] We know this exists because of the feature check above. - return dispatcher.useHostTransitionStatus(); + const status = dispatcher.useHostTransitionStatus(); + + dispatcher.useDebugValue(status); + + return status; } }