diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 9eab7a4a6b2e9..b4b0f97e2edaa 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -8,6 +8,7 @@ */ import type { + Awaited, ReactContext, ReactProviderType, StartTransitionOptions, @@ -80,11 +81,14 @@ function getPrimitiveStackCache(): Map> { // This type check is for Flow only. Dispatcher.useMemoCache(0); } - if (typeof Dispatcher.useOptimistic === 'function') { // This type check is for Flow only. Dispatcher.useOptimistic(null, (s: mixed, a: mixed) => s); } + if (typeof Dispatcher.useFormState === 'function') { + // This type check is for Flow only. + Dispatcher.useFormState((s: mixed, p: mixed) => s, null); + } } finally { readHookLog = hookLog; hookLog = []; @@ -372,6 +376,27 @@ function useOptimistic( return [state, (action: A) => {}]; } +function useFormState( + action: (Awaited, P) => S, + initialState: Awaited, + permalink?: string, +): [Awaited, (P) => void] { + const hook = nextHook(); // FormState + nextHook(); // ActionQueue + let state; + if (hook !== null) { + state = hook.memoizedState; + } else { + state = initialState; + } + hookLog.push({ + primitive: 'FormState', + stackError: new Error(), + value: state, + }); + return [state, (payload: P) => {}]; +} + const Dispatcher: DispatcherType = { use, readContext, @@ -393,6 +418,7 @@ const Dispatcher: DispatcherType = { useSyncExternalStore, useDeferredValue, useId, + useFormState, }; // create a proxy to throw a custom error diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index 9e1b4c35fe4af..ac4cb2e4a2b0f 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -11,6 +11,7 @@ 'use strict'; let React; +let ReactDOM; let ReactTestRenderer; let ReactDebugTools; let act; @@ -21,6 +22,7 @@ describe('ReactHooksInspectionIntegration', () => { jest.resetModules(); React = require('react'); ReactTestRenderer = require('react-test-renderer'); + ReactDOM = require('react-dom'); act = require('internal-test-utils').act; ReactDebugTools = require('react-debug-tools'); useMemoCache = React.unstable_useMemoCache; @@ -1144,4 +1146,44 @@ describe('ReactHooksInspectionIntegration', () => { }, ]); }); + + // @gate enableFormActions && enableAsyncActions + it('should support useFormState hook', () => { + function Foo() { + const [value] = ReactDOM.useFormState(function increment(n) { + return n; + }, 0); + React.useMemo(() => 'memo', []); + React.useMemo(() => 'not used', []); + + return value; + } + + const renderer = ReactTestRenderer.create(); + const childFiber = renderer.root.findByType(Foo)._currentFiber(); + const tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + expect(tree).toEqual([ + { + id: 0, + isStateEditable: false, + name: 'FormState', + value: 0, + subHooks: [], + }, + { + id: 1, + isStateEditable: false, + name: 'Memo', + value: 'memo', + subHooks: [], + }, + { + id: 2, + isStateEditable: false, + name: 'Memo', + value: 'not used', + subHooks: [], + }, + ]); + }); }); diff --git a/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js b/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js index 0c761b4a47136..2709b72ac2bef 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js @@ -20,6 +20,7 @@ import { useOptimistic, useState, } from 'react'; +import {useFormState} from 'react-dom'; const object = { string: 'abc', @@ -117,6 +118,18 @@ function wrapWithHoc(Component: (props: any, ref: React$Ref) => any) { } const HocWithHooks = wrapWithHoc(FunctionWithHooks); +function Forms() { + const [state, formAction] = useFormState((n: number, formData: FormData) => { + return n + 1; + }, 0); + return ( +
+ {state} + +
+ ); +} + export default function CustomHooks(): React.Node { return ( @@ -124,6 +137,7 @@ export default function CustomHooks(): React.Node { + ); }