Skip to content

Commit 874faf9

Browse files
author
Sebastian Silbermann
committed
DevTools: Add support for useFormStatus
1 parent 0775186 commit 874faf9

File tree

4 files changed

+220
-3
lines changed

4 files changed

+220
-3
lines changed

packages/react-debug-tools/src/ReactDebugHooks.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
Fiber,
2121
Dispatcher as DispatcherType,
2222
} from 'react-reconciler/src/ReactInternalTypes';
23+
import type {TransitionStatus} from 'react-reconciler/src/ReactFiberConfig';
2324

2425
import ErrorStackParser from 'error-stack-parser';
2526
import assign from 'shared/assign';
@@ -124,6 +125,11 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
124125
);
125126
} catch (x) {}
126127
}
128+
129+
if (typeof Dispatcher.useHostTransitionStatus === 'function') {
130+
// This type check is for Flow only.
131+
Dispatcher.useHostTransitionStatus();
132+
}
127133
} finally {
128134
readHookLog = hookLog;
129135
hookLog = [];
@@ -597,6 +603,19 @@ function useFormState<S, P>(
597603
return [state, (payload: P) => {}];
598604
}
599605

606+
function useHostTransitionStatus(): TransitionStatus {
607+
const status = readContext<TransitionStatus>({_currentValue: null});
608+
609+
hookLog.push({
610+
primitive: 'HostTransitionStatus',
611+
stackError: new Error(),
612+
value: status,
613+
debugInfo: null,
614+
});
615+
616+
return status;
617+
}
618+
600619
const Dispatcher: DispatcherType = {
601620
use,
602621
readContext,
@@ -619,6 +638,7 @@ const Dispatcher: DispatcherType = {
619638
useDeferredValue,
620639
useId,
621640
useFormState,
641+
useHostTransitionStatus,
622642
};
623643

624644
// create a proxy to throw a custom error
@@ -871,7 +891,8 @@ function buildTree(
871891
primitive === 'Context (use)' ||
872892
primitive === 'DebugValue' ||
873893
primitive === 'Promise' ||
874-
primitive === 'Unresolved'
894+
primitive === 'Unresolved' ||
895+
primitive === 'HostTransitionStatus'
875896
? null
876897
: nativeHookID++;
877898

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails react-core
8+
* @jest-environment jsdom
9+
*/
10+
11+
'use strict';
12+
13+
let React;
14+
let ReactDOM;
15+
let ReactDOMClient;
16+
let ReactDebugTools;
17+
let act;
18+
19+
function normalizeSourceLoc(tree) {
20+
tree.forEach(node => {
21+
if (node.hookSource) {
22+
node.hookSource.fileName = '**';
23+
node.hookSource.lineNumber = 0;
24+
node.hookSource.columnNumber = 0;
25+
}
26+
normalizeSourceLoc(node.subHooks);
27+
});
28+
return tree;
29+
}
30+
31+
describe('ReactHooksInspectionIntegration', () => {
32+
beforeEach(() => {
33+
jest.resetModules();
34+
React = require('react');
35+
ReactDOM = require('react-dom');
36+
ReactDOMClient = require('react-dom/client');
37+
act = require('internal-test-utils').act;
38+
ReactDebugTools = require('react-debug-tools');
39+
});
40+
41+
// @gate enableFormActions && enableAsyncActions
42+
it('should support useFormStatus hook', async () => {
43+
function FormStatus() {
44+
const status = ReactDOM.useFormStatus();
45+
React.useMemo(() => 'memo', []);
46+
React.useMemo(() => 'not used', []);
47+
48+
return JSON.stringify(status);
49+
}
50+
51+
const treeWithoutFiber = ReactDebugTools.inspectHooks(FormStatus);
52+
expect(normalizeSourceLoc(treeWithoutFiber)).toEqual([
53+
{
54+
debugInfo: null,
55+
hookSource: {
56+
columnNumber: 0,
57+
fileName: '**',
58+
functionName: 'FormStatus',
59+
lineNumber: 0,
60+
},
61+
id: null,
62+
isStateEditable: false,
63+
name: '.useFormStatus',
64+
subHooks: [
65+
{
66+
debugInfo: null,
67+
hookSource: {
68+
columnNumber: 0,
69+
fileName: '**',
70+
functionName: 'Object.useFormStatus',
71+
lineNumber: 0,
72+
},
73+
id: null,
74+
isStateEditable: false,
75+
name: 'HostTransitionStatus',
76+
subHooks: [],
77+
value: null,
78+
},
79+
],
80+
value: null,
81+
},
82+
{
83+
debugInfo: null,
84+
hookSource: {
85+
columnNumber: 0,
86+
fileName: '**',
87+
functionName: 'FormStatus',
88+
lineNumber: 0,
89+
},
90+
id: 0,
91+
isStateEditable: false,
92+
name: 'Memo',
93+
subHooks: [],
94+
value: 'memo',
95+
},
96+
{
97+
debugInfo: null,
98+
hookSource: {
99+
columnNumber: 0,
100+
fileName: '**',
101+
functionName: 'FormStatus',
102+
lineNumber: 0,
103+
},
104+
id: 1,
105+
isStateEditable: false,
106+
name: 'Memo',
107+
subHooks: [],
108+
value: 'not used',
109+
},
110+
]);
111+
112+
const root = ReactDOMClient.createRoot(document.createElement('div'));
113+
114+
await act(() => {
115+
root.render(
116+
<form>
117+
<FormStatus />
118+
</form>,
119+
);
120+
});
121+
122+
// Implementation detail. Feel free to adjust the position of the Fiber in the tree.
123+
const formStatusFiber = root._internalRoot.current.child.child;
124+
const treeWithFiber = ReactDebugTools.inspectHooksOfFiber(formStatusFiber);
125+
expect(normalizeSourceLoc(treeWithFiber)).toEqual([
126+
{
127+
debugInfo: null,
128+
hookSource: {
129+
columnNumber: 0,
130+
fileName: '**',
131+
functionName: 'FormStatus',
132+
lineNumber: 0,
133+
},
134+
id: null,
135+
isStateEditable: false,
136+
name: '.useFormStatus',
137+
subHooks: [
138+
{
139+
debugInfo: null,
140+
hookSource: {
141+
columnNumber: 0,
142+
fileName: '**',
143+
functionName: 'Object.useFormStatus',
144+
lineNumber: 0,
145+
},
146+
id: null,
147+
isStateEditable: false,
148+
name: 'HostTransitionStatus',
149+
subHooks: [],
150+
value: null,
151+
},
152+
],
153+
value: null,
154+
},
155+
{
156+
debugInfo: null,
157+
hookSource: {
158+
columnNumber: 0,
159+
fileName: '**',
160+
functionName: 'FormStatus',
161+
lineNumber: 0,
162+
},
163+
id: 0,
164+
isStateEditable: false,
165+
name: 'Memo',
166+
subHooks: [],
167+
value: 'memo',
168+
},
169+
{
170+
debugInfo: null,
171+
hookSource: {
172+
columnNumber: 0,
173+
fileName: '**',
174+
functionName: 'FormStatus',
175+
lineNumber: 0,
176+
},
177+
id: 1,
178+
isStateEditable: false,
179+
name: 'Memo',
180+
subHooks: [],
181+
value: 'not used',
182+
},
183+
]);
184+
});
185+
});

packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
useState,
2222
use,
2323
} from 'react';
24-
import {useFormState} from 'react-dom';
24+
import {useFormState, useFormStatus} from 'react-dom';
2525

2626
const object = {
2727
string: 'abc',
@@ -136,6 +136,12 @@ function incrementWithDelay(previousState: number, formData: FormData) {
136136
});
137137
}
138138

139+
function FormStatus() {
140+
const status = useFormStatus();
141+
142+
return <pre>{JSON.stringify(status)}</pre>;
143+
}
144+
139145
function Forms() {
140146
const [state, formAction] = useFormState<any, any>(incrementWithDelay, 0);
141147
return (
@@ -156,6 +162,7 @@ function Forms() {
156162
<input name="shouldReject" type="checkbox" />
157163
</label>
158164
<button formAction={formAction}>Increment</button>
165+
<FormStatus />
159166
</form>
160167
);
161168
}

packages/react-dom-bindings/src/shared/ReactDOMFormActions.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ export function useFormStatus(): FormStatus {
7272
} else {
7373
const dispatcher = resolveDispatcher();
7474
// $FlowFixMe[not-a-function] We know this exists because of the feature check above.
75-
return dispatcher.useHostTransitionStatus();
75+
const status = dispatcher.useHostTransitionStatus();
76+
77+
dispatcher.useDebugValue(status);
78+
79+
return status;
7680
}
7781
}
7882

0 commit comments

Comments
 (0)