Skip to content

Commit

Permalink
DevTools: Add support for useFormState (#28232)
Browse files Browse the repository at this point in the history
## 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)
  • Loading branch information
eps1lon authored Feb 5, 2024
1 parent 214fe84 commit 56cd10b
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 1 deletion.
28 changes: 27 additions & 1 deletion packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import type {
Awaited,
ReactContext,
ReactProviderType,
StartTransitionOptions,
Expand Down Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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,
Expand All @@ -393,6 +418,7 @@ const Dispatcher: DispatcherType = {
useSyncExternalStore,
useDeferredValue,
useId,
useFormState,
};

// create a proxy to throw a custom error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
'use strict';

let React;
let ReactDOM;
let ReactTestRenderer;
let ReactDebugTools;
let act;
Expand All @@ -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;
Expand Down Expand Up @@ -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: [],
},
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
useOptimistic,
useState,
} from 'react';
import {useFormState} from 'react-dom';

const object = {
string: 'abc',
Expand Down Expand Up @@ -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>
);
}
Expand Down

0 comments on commit 56cd10b

Please sign in to comment.