diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 962d7d8bba2b5..a3ca82c2571bf 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'; @@ -29,6 +30,7 @@ import { SimpleMemoComponent, ContextProvider, ForwardRef, + HostComponent, } from 'react-reconciler/src/ReactWorkTags'; import { REACT_MEMO_CACHE_SENTINEL, @@ -117,6 +119,11 @@ function getPrimitiveStackCache(): Map> { ); } catch (x) {} } + + if (typeof Dispatcher.useHostTransitionStatus === 'function') { + // This type check is for Flow only. + Dispatcher.useHostTransitionStatus(); + } } finally { readHookLog = hookLog; hookLog = []; @@ -509,6 +516,40 @@ function useFormState( return [state, (payload: P) => {}]; } +function useHostTransitionStatus(): TransitionStatus { + // The type is host specific. This is just a placeholder. + let status: mixed = null; + + if (currentFiber !== null) { + const dependencies = currentFiber.dependencies; + if (dependencies !== null) { + let contextDependency = dependencies.firstContext; + while (contextDependency !== null) { + if ( + // $FlowFixMe -- TODO + contextDependency.context.$$contextType === + Symbol.for('react.HostTransitionContext') + ) { + // TODO: This as well as contextDependency.context.currentValue is null because setupContext does not set the value yet + status = contextDependency.memoizedValue; + } + contextDependency = contextDependency.next; + } + } + } + + // TODO: Handle host transition context not found. + + hookLog.push({ + primitive: 'HostTransitionStatus', + stackError: new Error(), + value: status, + debugInfo: null, + }); + + return ((status: any): TransitionStatus); +} + const Dispatcher: DispatcherType = { use, readContext, @@ -531,6 +572,7 @@ const Dispatcher: DispatcherType = { useDeferredValue, useId, useFormState, + useHostTransitionStatus, }; // create a proxy to throw a custom error @@ -787,7 +829,8 @@ function buildTree( primitive === 'Context (use)' || primitive === 'DebugValue' || primitive === 'Promise' || - primitive === 'Unresolved' + primitive === 'Unresolved' || + primitive === 'HostTransitionStatus' ? null : nativeHookID++; @@ -939,6 +982,8 @@ function setupContexts(contextMap: Map, any>, fiber: Fiber) { // Set the inner most provider value on the context. context._currentValue = current.memoizedProps.value; } + } else if (current.tag === HostComponent) { + // TODO: Set the HostTransitionContext value here? } current = current.return; } diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index b6224f2dfa76e..02d0901fa9f2c 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -1332,4 +1332,54 @@ describe('ReactHooksInspectionIntegration', () => { }, ]); }); + + // @gate enableFormActions && enableAsyncActions + it('should support useFormStatus hook', () => { + function Foo() { + const status = ReactDOM.useFormStatus(); + React.useMemo(() => 'memo', []); + React.useMemo(() => 'not used', []); + + return JSON.stringify(status); + } + + const renderer = ReactTestRenderer.create(); + const childFiber = renderer.root.findByType(Foo)._currentFiber(); + const tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + expect(tree).toEqual([ + { + debugInfo: null, + id: null, + isStateEditable: false, + name: '.useFormStatus', + subHooks: [ + { + debugInfo: null, + id: null, + isStateEditable: false, + name: 'HostTransitionStatus', + subHooks: [], + value: null, + }, + ], + value: null, + }, + { + debugInfo: null, + id: 0, + isStateEditable: false, + name: 'Memo', + subHooks: [], + value: 'memo', + }, + { + debugInfo: null, + 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 aa3a9a6295731..2f07b72001602 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', @@ -120,18 +120,69 @@ function wrapWithHoc(Component: (props: any, ref: React$Ref) => any) { } const HocWithHooks = wrapWithHoc(FunctionWithHooks); +function incrementWithDelay(previousState: number, formData: FormData) { + const incrementDelay = +formData.get('incrementDelay'); + const shouldReject = formData.get('shouldReject'); + const reason = formData.get('reason'); + + return new Promise((resolve, reject) => { + setTimeout(() => { + if (shouldReject) { + reject(reason); + } else { + resolve(previousState + 1); + } + }, incrementDelay); + }); +} + +function FormStatus() { + const status = useFormStatus(); + + return JSON.stringify(status); +} + function Forms() { - const [state, formAction] = useFormState((n: number, formData: FormData) => { - return n + 1; - }, 0); + const [state, formAction] = useFormState(incrementWithDelay, 0); return (
- {state} + State: {state}  + + + ); } +class ErrorBoundary extends React.Component<{children?: React$Node}> { + state: {error: any} = {error: null}; + static getDerivedStateFromError(error: mixed): {error: any} { + return {error}; + } + componentDidCatch(error: any, info: any) { + console.error(error, info); + } + render(): any { + if (this.state.error) { + return
Error: {String(this.state.error)}
; + } + return this.props.children; + } +} + export default function CustomHooks(): React.Node { return ( @@ -139,7 +190,9 @@ export default function CustomHooks(): React.Node { - + + + ); } diff --git a/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js b/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js index b2ac098bcee70..6d7e740b1ef19 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; } } diff --git a/packages/react-reconciler/src/ReactFiberHostContext.js b/packages/react-reconciler/src/ReactFiberHostContext.js index 3ba99edb239ad..6e4a737f79785 100644 --- a/packages/react-reconciler/src/ReactFiberHostContext.js +++ b/packages/react-reconciler/src/ReactFiberHostContext.js @@ -53,6 +53,9 @@ export const HostTransitionContext: ReactContext = { Consumer: (null: any), }; +// $FlowFixMe -- TODO +HostTransitionContext.$$contextType = Symbol.for('react.HostTransitionContext'); + function requiredContext(c: Value | null): Value { if (__DEV__) { if (c === null) {