Skip to content

Commit 26474b8

Browse files
author
Sebastian Silbermann
committed
WIP DevTools: Add support for useFormStatus
1 parent edf0d35 commit 26474b8

File tree

5 files changed

+163
-8
lines changed

5 files changed

+163
-8
lines changed

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

Lines changed: 46 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';
@@ -29,6 +30,7 @@ import {
2930
SimpleMemoComponent,
3031
ContextProvider,
3132
ForwardRef,
33+
HostComponent,
3234
} from 'react-reconciler/src/ReactWorkTags';
3335
import {
3436
REACT_MEMO_CACHE_SENTINEL,
@@ -117,6 +119,11 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
117119
);
118120
} catch (x) {}
119121
}
122+
123+
if (typeof Dispatcher.useHostTransitionStatus === 'function') {
124+
// This type check is for Flow only.
125+
Dispatcher.useHostTransitionStatus();
126+
}
120127
} finally {
121128
readHookLog = hookLog;
122129
hookLog = [];
@@ -509,6 +516,40 @@ function useFormState<S, P>(
509516
return [state, (payload: P) => {}];
510517
}
511518

519+
function useHostTransitionStatus(): TransitionStatus {
520+
// The type is host specific. This is just a placeholder.
521+
let status: mixed = null;
522+
523+
if (currentFiber !== null) {
524+
const dependencies = currentFiber.dependencies;
525+
if (dependencies !== null) {
526+
let contextDependency = dependencies.firstContext;
527+
while (contextDependency !== null) {
528+
if (
529+
// $FlowFixMe -- TODO
530+
contextDependency.context.$$contextType ===
531+
Symbol.for('react.HostTransitionContext')
532+
) {
533+
// TODO: This as well as contextDependency.context.currentValue is null because setupContext does not set the value yet
534+
status = contextDependency.memoizedValue;
535+
}
536+
contextDependency = contextDependency.next;
537+
}
538+
}
539+
}
540+
541+
// TODO: Handle host transition context not found.
542+
543+
hookLog.push({
544+
primitive: 'HostTransitionStatus',
545+
stackError: new Error(),
546+
value: status,
547+
debugInfo: null,
548+
});
549+
550+
return ((status: any): TransitionStatus);
551+
}
552+
512553
const Dispatcher: DispatcherType = {
513554
use,
514555
readContext,
@@ -531,6 +572,7 @@ const Dispatcher: DispatcherType = {
531572
useDeferredValue,
532573
useId,
533574
useFormState,
575+
useHostTransitionStatus,
534576
};
535577

536578
// create a proxy to throw a custom error
@@ -787,7 +829,8 @@ function buildTree(
787829
primitive === 'Context (use)' ||
788830
primitive === 'DebugValue' ||
789831
primitive === 'Promise' ||
790-
primitive === 'Unresolved'
832+
primitive === 'Unresolved' ||
833+
primitive === 'HostTransitionStatus'
791834
? null
792835
: nativeHookID++;
793836

@@ -939,6 +982,8 @@ function setupContexts(contextMap: Map<ReactContext<any>, any>, fiber: Fiber) {
939982
// Set the inner most provider value on the context.
940983
context._currentValue = current.memoizedProps.value;
941984
}
985+
} else if (current.tag === HostComponent) {
986+
// TODO: Set the HostTransitionContext value here?
942987
}
943988
current = current.return;
944989
}

packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,4 +1332,54 @@ describe('ReactHooksInspectionIntegration', () => {
13321332
},
13331333
]);
13341334
});
1335+
1336+
// @gate enableFormActions && enableAsyncActions
1337+
it('should support useFormStatus hook', () => {
1338+
function Foo() {
1339+
const status = ReactDOM.useFormStatus();
1340+
React.useMemo(() => 'memo', []);
1341+
React.useMemo(() => 'not used', []);
1342+
1343+
return JSON.stringify(status);
1344+
}
1345+
1346+
const renderer = ReactTestRenderer.create(<Foo />);
1347+
const childFiber = renderer.root.findByType(Foo)._currentFiber();
1348+
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
1349+
expect(tree).toEqual([
1350+
{
1351+
debugInfo: null,
1352+
id: null,
1353+
isStateEditable: false,
1354+
name: '.useFormStatus',
1355+
subHooks: [
1356+
{
1357+
debugInfo: null,
1358+
id: null,
1359+
isStateEditable: false,
1360+
name: 'HostTransitionStatus',
1361+
subHooks: [],
1362+
value: null,
1363+
},
1364+
],
1365+
value: null,
1366+
},
1367+
{
1368+
debugInfo: null,
1369+
id: 0,
1370+
isStateEditable: false,
1371+
name: 'Memo',
1372+
subHooks: [],
1373+
value: 'memo',
1374+
},
1375+
{
1376+
debugInfo: null,
1377+
id: 1,
1378+
isStateEditable: false,
1379+
name: 'Memo',
1380+
subHooks: [],
1381+
value: 'not used',
1382+
},
1383+
]);
1384+
});
13351385
});

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

Lines changed: 59 additions & 6 deletions
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',
@@ -120,26 +120,79 @@ function wrapWithHoc(Component: (props: any, ref: React$Ref<any>) => any) {
120120
}
121121
const HocWithHooks = wrapWithHoc(FunctionWithHooks);
122122

123+
function incrementWithDelay(previousState: number, formData: FormData) {
124+
const incrementDelay = +formData.get('incrementDelay');
125+
const shouldReject = formData.get('shouldReject');
126+
const reason = formData.get('reason');
127+
128+
return new Promise((resolve, reject) => {
129+
setTimeout(() => {
130+
if (shouldReject) {
131+
reject(reason);
132+
} else {
133+
resolve(previousState + 1);
134+
}
135+
}, incrementDelay);
136+
});
137+
}
138+
139+
function FormStatus() {
140+
const status = useFormStatus();
141+
142+
return JSON.stringify(status);
143+
}
144+
123145
function Forms() {
124-
const [state, formAction] = useFormState((n: number, formData: FormData) => {
125-
return n + 1;
126-
}, 0);
146+
const [state, formAction] = useFormState<any, any>(incrementWithDelay, 0);
127147
return (
128148
<form>
129-
{state}
149+
State: {state}&nbsp;
150+
<label>
151+
delay:
152+
<input
153+
name="incrementDelay"
154+
defaultValue={5000}
155+
type="text"
156+
inputMode="numeric"
157+
/>
158+
</label>
159+
<label>
160+
Reject:
161+
<input name="reason" type="text" />
162+
<input name="shouldReject" type="checkbox" />
163+
</label>
130164
<button formAction={formAction}>Increment</button>
165+
<FormStatus />
131166
</form>
132167
);
133168
}
134169

170+
class ErrorBoundary extends React.Component<{children?: React$Node}> {
171+
state: {error: any} = {error: null};
172+
static getDerivedStateFromError(error: mixed): {error: any} {
173+
return {error};
174+
}
175+
componentDidCatch(error: any, info: any) {
176+
console.error(error, info);
177+
}
178+
render(): any {
179+
if (this.state.error) {
180+
return <div>Error: {String(this.state.error)}</div>;
181+
}
182+
return this.props.children;
183+
}
184+
}
185+
135186
export default function CustomHooks(): React.Node {
136187
return (
137188
<Fragment>
138189
<FunctionWithHooks />
139190
<MemoWithHooks />
140191
<ForwardRefWithHooks />
141192
<HocWithHooks />
142-
<Forms />
193+
<ErrorBoundary>
194+
<Forms />
195+
</ErrorBoundary>
143196
</Fragment>
144197
);
145198
}

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

packages/react-reconciler/src/ReactFiberHostContext.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ export const HostTransitionContext: ReactContext<TransitionStatus | null> = {
5353
Consumer: (null: any),
5454
};
5555

56+
// $FlowFixMe -- TODO
57+
HostTransitionContext.$$contextType = Symbol.for('react.HostTransitionContext');
58+
5659
function requiredContext<Value>(c: Value | null): Value {
5760
if (__DEV__) {
5861
if (c === null) {

0 commit comments

Comments
 (0)