DevTools: Add support for useFormState (#28232)
## 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 
Sebastian Silbermann committed
Feb 5, 2024 at 15:39 UTC
56cd10beb40586d09e91157e8f6ac531478a62be
3 files changed
+83
-1
packages/react-debug-tools/src/ReactDebugHooks.js
+27
-1
@@ -8,6 +8,7 @@
8
*/
9
10
import type {
11
+ Awaited,
12
ReactContext,
13
ReactProviderType,
14
StartTransitionOptions,
@@ -80,11 +81,14 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
81
// This type check is for Flow only.
82
Dispatcher.useMemoCache(0);
83
}
83
-
84
if (typeof Dispatcher.useOptimistic === 'function') {
85
// This type check is for Flow only.
86
Dispatcher.useOptimistic(null, (s: mixed, a: mixed) => s);
87
}
88
+ if (typeof Dispatcher.useFormState === 'function') {
89
+ // This type check is for Flow only.
90
+ Dispatcher.useFormState((s: mixed, p: mixed) => s, null);
91
+ }
92
} finally {
93
readHookLog = hookLog;
94
hookLog = [];
@@ -372,6 +376,27 @@ function useOptimistic<S, A>(
376
return [state, (action: A) => {}];
377
}
378
379
+function useFormState<S, P>(
380
+ action: (Awaited<S>, P) => S,
381
+ initialState: Awaited<S>,
382
+ permalink?: string,
383
+): [Awaited<S>, (P) => void] {
384
+ const hook = nextHook(); // FormState
385
+ nextHook(); // ActionQueue
386
+ let state;
387
+ if (hook !== null) {
388
+ state = hook.memoizedState;
389
+ } else {
390
+ state = initialState;
391
+ }
392
+ hookLog.push({
393
+ primitive: 'FormState',
394
+ stackError: new Error(),
395
+ value: state,
396
+ });
397
+ return [state, (payload: P) => {}];
398
+}
399
+
400
const Dispatcher: DispatcherType = {
401
use,
402
readContext,
@@ -393,6 +418,7 @@ const Dispatcher: DispatcherType = {
418
useSyncExternalStore,
419
useDeferredValue,
420
useId,
421
+ useFormState,
422
};
423
424
// create a proxy to throw a custom error
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+42
@@ -11,6 +11,7 @@
11
'use strict';
12
13
let React;
14
+let ReactDOM;
15
let ReactTestRenderer;
16
let ReactDebugTools;
17
let act;
@@ -21,6 +22,7 @@ describe('ReactHooksInspectionIntegration', () => {
22
jest.resetModules();
23
React = require('react');
24
ReactTestRenderer = require('react-test-renderer');
25
+ ReactDOM = require('react-dom');
26
act = require('internal-test-utils').act;
27
ReactDebugTools = require('react-debug-tools');
28
useMemoCache = React.unstable_useMemoCache;
@@ -1144,4 +1146,44 @@ describe('ReactHooksInspectionIntegration', () => {
1146
},
1147
]);
1148
});
1149
+
1150
+ // @gate enableFormActions && enableAsyncActions
1151
+ it('should support useFormState hook', () => {
1152
+ function Foo() {
1153
+ const [value] = ReactDOM.useFormState(function increment(n) {
1154
+ return n;
1155
+ }, 0);
1156
+ React.useMemo(() => 'memo', []);
1157
+ React.useMemo(() => 'not used', []);
1158
+
1159
+ return value;
1160
+ }
1161
+
1162
+ const renderer = ReactTestRenderer.create(<Foo />);
1163
+ const childFiber = renderer.root.findByType(Foo)._currentFiber();
1164
+ const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
1165
+ expect(tree).toEqual([
1166
+ {
1167
+ id: 0,
1168
+ isStateEditable: false,
1169
+ name: 'FormState',
1170
+ value: 0,
1171
+ subHooks: [],
1172
+ },
1173
+ {
1174
+ id: 1,
1175
+ isStateEditable: false,
1176
+ name: 'Memo',
1177
+ value: 'memo',
1178
+ subHooks: [],
1179
+ },
1180
+ {
1181
+ id: 2,
1182
+ isStateEditable: false,
1183
+ name: 'Memo',
1184
+ value: 'not used',
1185
+ subHooks: [],
1186
+ },
1187
+ ]);
1188
+ });
1189
});
packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js
+14
@@ -20,6 +20,7 @@ import {
20
useOptimistic,
21
useState,
22
} from 'react';
23
+import {useFormState} from 'react-dom';
24
25
const object = {
26
string: 'abc',
@@ -117,6 +118,18 @@ function wrapWithHoc(Component: (props: any, ref: React$Ref<any>) => any) {
118
}
119
const HocWithHooks = wrapWithHoc(FunctionWithHooks);
120
121
+function Forms() {
122
+ const [state, formAction] = useFormState((n: number, formData: FormData) => {
123
+ return n + 1;
124
+ }, 0);
125
+ return (
126
+ <form>
127
+ {state}
128
+ <button formAction={formAction}>Increment</button>
129
+ </form>
130
+ );
131
+}
132
+
133
export default function CustomHooks(): React.Node {
134
return (
135
<Fragment>
@@ -124,6 +137,7 @@ export default function CustomHooks(): React.Node {
137
<MemoWithHooks />
138
<ForwardRefWithHooks />
139
<HocWithHooks />
140
+ <Forms />
141
</Fragment>
142
);
143
}