mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
Devtools: Add support for useFormStatus (#28413)
This commit is contained in:
committed by
GitHub
parent
bc8cc6e2ed
commit
4f6f4a2ade
+35
-7
@@ -21,6 +21,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';
|
||||
@@ -134,6 +135,11 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
|
||||
}
|
||||
|
||||
Dispatcher.useId();
|
||||
|
||||
if (typeof Dispatcher.useHostTransitionStatus === 'function') {
|
||||
// This type check is for Flow only.
|
||||
Dispatcher.useHostTransitionStatus();
|
||||
}
|
||||
} finally {
|
||||
readHookLog = hookLog;
|
||||
hookLog = [];
|
||||
@@ -711,6 +717,27 @@ function useActionState<S, P>(
|
||||
return [state, (payload: P) => {}, false];
|
||||
}
|
||||
|
||||
function useHostTransitionStatus(): TransitionStatus {
|
||||
const status = readContext<TransitionStatus>(
|
||||
// $FlowFixMe[prop-missing] `readContext` only needs _currentValue
|
||||
({
|
||||
// $FlowFixMe[incompatible-cast] TODO: Incorrect bottom value without access to Fiber config.
|
||||
_currentValue: null,
|
||||
}: ReactContext<TransitionStatus>),
|
||||
);
|
||||
|
||||
hookLog.push({
|
||||
displayName: null,
|
||||
primitive: 'HostTransitionStatus',
|
||||
stackError: new Error(),
|
||||
value: status,
|
||||
debugInfo: null,
|
||||
dispatcherHookName: 'HostTransitionStatus',
|
||||
});
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
const Dispatcher: DispatcherType = {
|
||||
use,
|
||||
readContext,
|
||||
@@ -734,6 +761,7 @@ const Dispatcher: DispatcherType = {
|
||||
useId,
|
||||
useFormState,
|
||||
useActionState,
|
||||
useHostTransitionStatus,
|
||||
};
|
||||
|
||||
// create a proxy to throw a custom error
|
||||
@@ -854,12 +882,11 @@ function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) {
|
||||
isReactWrapper(hookStack[i].functionName, hook.dispatcherHookName)
|
||||
) {
|
||||
i++;
|
||||
}
|
||||
if (
|
||||
i < hookStack.length - 1 &&
|
||||
isReactWrapper(hookStack[i].functionName, hook.dispatcherHookName)
|
||||
) {
|
||||
i++;
|
||||
// Guard against the dispatcher call being inlined.
|
||||
// At this point we wouldn't be able to recover the actual React Hook name.
|
||||
if (i < hookStack.length - 1) {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
@@ -997,7 +1024,8 @@ function buildTree(
|
||||
primitive === 'Context (use)' ||
|
||||
primitive === 'DebugValue' ||
|
||||
primitive === 'Promise' ||
|
||||
primitive === 'Unresolved'
|
||||
primitive === 'Unresolved' ||
|
||||
primitive === 'HostTransitionStatus'
|
||||
? null
|
||||
: nativeHookID++;
|
||||
|
||||
|
||||
@@ -426,6 +426,150 @@ describe('ReactHooksInspection', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not confuse built-in hooks with custom hooks that have the same name', () => {
|
||||
function useState(value) {
|
||||
React.useState(value);
|
||||
React.useDebugValue('custom useState');
|
||||
}
|
||||
function useFormStatus() {
|
||||
React.useState('custom useState');
|
||||
React.useDebugValue('custom useFormStatus');
|
||||
}
|
||||
function Foo(props) {
|
||||
useFormStatus();
|
||||
useState('Hello, Dave!');
|
||||
return null;
|
||||
}
|
||||
const tree = ReactDebugTools.inspectHooks(Foo, {});
|
||||
if (__DEV__) {
|
||||
expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"debugInfo": null,
|
||||
"hookSource": {
|
||||
"columnNumber": 0,
|
||||
"fileName": "**",
|
||||
"functionName": "Foo",
|
||||
"lineNumber": 0,
|
||||
},
|
||||
"id": null,
|
||||
"isStateEditable": false,
|
||||
"name": "FormStatus",
|
||||
"subHooks": [
|
||||
{
|
||||
"debugInfo": null,
|
||||
"hookSource": {
|
||||
"columnNumber": 0,
|
||||
"fileName": "**",
|
||||
"functionName": "useFormStatus",
|
||||
"lineNumber": 0,
|
||||
},
|
||||
"id": 0,
|
||||
"isStateEditable": true,
|
||||
"name": "State",
|
||||
"subHooks": [],
|
||||
"value": "custom useState",
|
||||
},
|
||||
],
|
||||
"value": "custom useFormStatus",
|
||||
},
|
||||
{
|
||||
"debugInfo": null,
|
||||
"hookSource": {
|
||||
"columnNumber": 0,
|
||||
"fileName": "**",
|
||||
"functionName": "Foo",
|
||||
"lineNumber": 0,
|
||||
},
|
||||
"id": null,
|
||||
"isStateEditable": false,
|
||||
"name": "State",
|
||||
"subHooks": [
|
||||
{
|
||||
"debugInfo": null,
|
||||
"hookSource": {
|
||||
"columnNumber": 0,
|
||||
"fileName": "**",
|
||||
"functionName": "useState",
|
||||
"lineNumber": 0,
|
||||
},
|
||||
"id": 1,
|
||||
"isStateEditable": true,
|
||||
"name": "State",
|
||||
"subHooks": [],
|
||||
"value": "Hello, Dave!",
|
||||
},
|
||||
],
|
||||
"value": "custom useState",
|
||||
},
|
||||
]
|
||||
`);
|
||||
} else {
|
||||
expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"debugInfo": null,
|
||||
"hookSource": {
|
||||
"columnNumber": 0,
|
||||
"fileName": "**",
|
||||
"functionName": "Foo",
|
||||
"lineNumber": 0,
|
||||
},
|
||||
"id": null,
|
||||
"isStateEditable": false,
|
||||
"name": "FormStatus",
|
||||
"subHooks": [
|
||||
{
|
||||
"debugInfo": null,
|
||||
"hookSource": {
|
||||
"columnNumber": 0,
|
||||
"fileName": "**",
|
||||
"functionName": "useFormStatus",
|
||||
"lineNumber": 0,
|
||||
},
|
||||
"id": 0,
|
||||
"isStateEditable": true,
|
||||
"name": "State",
|
||||
"subHooks": [],
|
||||
"value": "custom useState",
|
||||
},
|
||||
],
|
||||
"value": undefined,
|
||||
},
|
||||
{
|
||||
"debugInfo": null,
|
||||
"hookSource": {
|
||||
"columnNumber": 0,
|
||||
"fileName": "**",
|
||||
"functionName": "Foo",
|
||||
"lineNumber": 0,
|
||||
},
|
||||
"id": null,
|
||||
"isStateEditable": false,
|
||||
"name": "State",
|
||||
"subHooks": [
|
||||
{
|
||||
"debugInfo": null,
|
||||
"hookSource": {
|
||||
"columnNumber": 0,
|
||||
"fileName": "**",
|
||||
"functionName": "useState",
|
||||
"lineNumber": 0,
|
||||
},
|
||||
"id": 1,
|
||||
"isStateEditable": true,
|
||||
"name": "State",
|
||||
"subHooks": [],
|
||||
"value": "Hello, Dave!",
|
||||
},
|
||||
],
|
||||
"value": undefined,
|
||||
},
|
||||
]
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should inspect the default value using the useContext hook', () => {
|
||||
const MyContext = React.createContext('default');
|
||||
function Foo(props) {
|
||||
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let ReactDOMClient;
|
||||
let ReactDebugTools;
|
||||
let act;
|
||||
|
||||
function normalizeSourceLoc(tree) {
|
||||
tree.forEach(node => {
|
||||
if (node.hookSource) {
|
||||
node.hookSource.fileName = '**';
|
||||
node.hookSource.lineNumber = 0;
|
||||
node.hookSource.columnNumber = 0;
|
||||
}
|
||||
normalizeSourceLoc(node.subHooks);
|
||||
});
|
||||
return tree;
|
||||
}
|
||||
|
||||
describe('ReactHooksInspectionIntegration', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMClient = require('react-dom/client');
|
||||
act = require('internal-test-utils').act;
|
||||
ReactDebugTools = require('react-debug-tools');
|
||||
});
|
||||
|
||||
it('should support useFormStatus hook', async () => {
|
||||
function FormStatus() {
|
||||
const status = ReactDOM.useFormStatus();
|
||||
React.useMemo(() => 'memo', []);
|
||||
React.useMemo(() => 'not used', []);
|
||||
|
||||
return JSON.stringify(status);
|
||||
}
|
||||
|
||||
const treeWithoutFiber = ReactDebugTools.inspectHooks(FormStatus);
|
||||
expect(normalizeSourceLoc(treeWithoutFiber)).toEqual([
|
||||
{
|
||||
debugInfo: null,
|
||||
hookSource: {
|
||||
columnNumber: 0,
|
||||
fileName: '**',
|
||||
functionName: 'FormStatus',
|
||||
lineNumber: 0,
|
||||
},
|
||||
id: null,
|
||||
isStateEditable: false,
|
||||
name: 'FormStatus',
|
||||
subHooks: [],
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
debugInfo: null,
|
||||
hookSource: {
|
||||
columnNumber: 0,
|
||||
fileName: '**',
|
||||
functionName: 'FormStatus',
|
||||
lineNumber: 0,
|
||||
},
|
||||
id: 0,
|
||||
isStateEditable: false,
|
||||
name: 'Memo',
|
||||
subHooks: [],
|
||||
value: 'memo',
|
||||
},
|
||||
{
|
||||
debugInfo: null,
|
||||
hookSource: {
|
||||
columnNumber: 0,
|
||||
fileName: '**',
|
||||
functionName: 'FormStatus',
|
||||
lineNumber: 0,
|
||||
},
|
||||
id: 1,
|
||||
isStateEditable: false,
|
||||
name: 'Memo',
|
||||
subHooks: [],
|
||||
value: 'not used',
|
||||
},
|
||||
]);
|
||||
|
||||
const root = ReactDOMClient.createRoot(document.createElement('div'));
|
||||
|
||||
await act(() => {
|
||||
root.render(
|
||||
<form>
|
||||
<FormStatus />
|
||||
</form>,
|
||||
);
|
||||
});
|
||||
|
||||
// Implementation detail. Feel free to adjust the position of the Fiber in the tree.
|
||||
const formStatusFiber = root._internalRoot.current.child.child;
|
||||
const treeWithFiber = ReactDebugTools.inspectHooksOfFiber(formStatusFiber);
|
||||
expect(normalizeSourceLoc(treeWithFiber)).toEqual([
|
||||
{
|
||||
debugInfo: null,
|
||||
hookSource: {
|
||||
columnNumber: 0,
|
||||
fileName: '**',
|
||||
functionName: 'FormStatus',
|
||||
lineNumber: 0,
|
||||
},
|
||||
id: null,
|
||||
isStateEditable: false,
|
||||
name: 'FormStatus',
|
||||
subHooks: [],
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
debugInfo: null,
|
||||
hookSource: {
|
||||
columnNumber: 0,
|
||||
fileName: '**',
|
||||
functionName: 'FormStatus',
|
||||
lineNumber: 0,
|
||||
},
|
||||
id: 0,
|
||||
isStateEditable: false,
|
||||
name: 'Memo',
|
||||
subHooks: [],
|
||||
value: 'memo',
|
||||
},
|
||||
{
|
||||
debugInfo: null,
|
||||
hookSource: {
|
||||
columnNumber: 0,
|
||||
fileName: '**',
|
||||
functionName: 'FormStatus',
|
||||
lineNumber: 0,
|
||||
},
|
||||
id: 1,
|
||||
isStateEditable: false,
|
||||
name: 'Memo',
|
||||
subHooks: [],
|
||||
value: 'not used',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
useState,
|
||||
use,
|
||||
} from 'react';
|
||||
import {useFormState} from 'react-dom';
|
||||
import {useFormState, useFormStatus} from 'react-dom';
|
||||
|
||||
const object = {
|
||||
string: 'abc',
|
||||
@@ -164,6 +164,12 @@ function incrementWithDelay(previousState: number, formData: FormData) {
|
||||
});
|
||||
}
|
||||
|
||||
function FormStatus() {
|
||||
const status = useFormStatus();
|
||||
|
||||
return <pre>{JSON.stringify(status)}</pre>;
|
||||
}
|
||||
|
||||
function Forms() {
|
||||
const [state, formAction] = useFormState<any, any>(incrementWithDelay, 0);
|
||||
return (
|
||||
@@ -184,6 +190,7 @@ function Forms() {
|
||||
<input name="shouldReject" type="checkbox" />
|
||||
</label>
|
||||
<button formAction={formAction}>Increment</button>
|
||||
<FormStatus />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user