Repository: facebook/react
Pull request: DevTools: Add support for useFormState


## Summary

Add support for `useFormState` Hook fixing "Unsupported hook in the react-debug-tools package: Missing method in Dispatcher: useFormState" when inspecting components using `useFormState`

## How did you test this change?

- Added test to ReactHooksInspectionIntegration
- Added dedicated section for form actions to devtools-shell
   ![Screenshot 2024-02-04 at 12 02 05](https://github.com/facebook/react/assets/12292047/bb274789-64b8-4594-963e-87c4b6962144)
   

Changed files:
filename: packages/react-debug-tools/src/ReactDebugHooks.js
patch: @@ -8,6 +8,7 @@
  */
 
 import type {
+  Awaited,
   ReactContext,
   ReactProviderType,
   StartTransitionOptions,
@@ -80,11 +81,14 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
         // 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<S, A>(
   return [state, (action: A) => {}];
 }
 
+function useFormState<S, P>(
+  action: (Awaited<S>, P) => S,
+  initialState: Awaited<S>,
+  permalink?: string,
+): [Awaited<S>, (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

filename: packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
patch: @@ -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(<Foo />);
+    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: [],
+      },
+    ]);
+  });
 });

filename: packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js
patch: @@ -20,6 +20,7 @@ import {
   useOptimistic,
   useState,
 } from 'react';
+import {useFormState} from 'react-dom';
 
 const object = {
   string: 'abc',
@@ -117,13 +118,26 @@ function wrapWithHoc(Component: (props: any, ref: React$Ref<any>) => any) {
 }
 const HocWithHooks = wrapWithHoc(FunctionWithHooks);
 
+function Forms() {
+  const [state, formAction] = useFormState((n: number, formData: FormData) => {
+    return n + 1;
+  }, 0);
+  return (
+    <form>
+      {state}
+      <button formAction={formAction}>Increment</button>
+    </form>
+  );
+}
+
 export default function CustomHooks(): React.Node {
   return (
     <Fragment>
       <FunctionWithHooks />
       <MemoWithHooks />
       <ForwardRefWithHooks />
       <HocWithHooks />
+      <Forms />
     </Fragment>
   );
 }