Skip to content

Commit fb10a2c

Browse files
authored
Devtools: Unwrap Promise in useFormState (#28319)
1 parent 01ab35a commit fb10a2c

File tree

2 files changed

+100
-11
lines changed

2 files changed

+100
-11
lines changed

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

+49-6
Original file line numberDiff line numberDiff line change
@@ -521,19 +521,62 @@ function useFormState<S, P>(
521521
): [Awaited<S>, (P) => void] {
522522
const hook = nextHook(); // FormState
523523
nextHook(); // ActionQueue
524-
let state;
524+
const stackError = new Error();
525+
let value;
526+
let debugInfo = null;
527+
let error = null;
528+
525529
if (hook !== null) {
526-
state = hook.memoizedState;
530+
const actionResult = hook.memoizedState;
531+
if (
532+
typeof actionResult === 'object' &&
533+
actionResult !== null &&
534+
// $FlowFixMe[method-unbinding]
535+
typeof actionResult.then === 'function'
536+
) {
537+
const thenable: Thenable<Awaited<S>> = (actionResult: any);
538+
switch (thenable.status) {
539+
case 'fulfilled': {
540+
value = thenable.value;
541+
debugInfo =
542+
thenable._debugInfo === undefined ? null : thenable._debugInfo;
543+
break;
544+
}
545+
case 'rejected': {
546+
const rejectedError = thenable.reason;
547+
error = rejectedError;
548+
break;
549+
}
550+
default:
551+
// If this was an uncached Promise we have to abandon this attempt
552+
// but we can still emit anything up until this point.
553+
error = SuspenseException;
554+
debugInfo =
555+
thenable._debugInfo === undefined ? null : thenable._debugInfo;
556+
value = thenable;
557+
}
558+
} else {
559+
value = (actionResult: any);
560+
}
527561
} else {
528-
state = initialState;
562+
value = initialState;
529563
}
564+
530565
hookLog.push({
531566
displayName: null,
532567
primitive: 'FormState',
533-
stackError: new Error(),
534-
value: state,
535-
debugInfo: null,
568+
stackError: stackError,
569+
value: value,
570+
debugInfo: debugInfo,
536571
});
572+
573+
if (error !== null) {
574+
throw error;
575+
}
576+
577+
// value being a Thenable is equivalent to error being not null
578+
// i.e. we only reach this point with Awaited<S>
579+
const state = ((value: any): Awaited<S>);
537580
return [state, (payload: P) => {}];
538581
}
539582

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

+51-5
Original file line numberDiff line numberDiff line change
@@ -120,26 +120,72 @@ 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+
123139
function Forms() {
124-
const [state, formAction] = useFormState((n: number, formData: FormData) => {
125-
return n + 1;
126-
}, 0);
140+
const [state, formAction] = useFormState<any, any>(incrementWithDelay, 0);
127141
return (
128142
<form>
129-
{state}
143+
State: {state}&nbsp;
144+
<label>
145+
delay:
146+
<input
147+
name="incrementDelay"
148+
defaultValue={5000}
149+
type="text"
150+
inputMode="numeric"
151+
/>
152+
</label>
153+
<label>
154+
Reject:
155+
<input name="reason" type="text" />
156+
<input name="shouldReject" type="checkbox" />
157+
</label>
130158
<button formAction={formAction}>Increment</button>
131159
</form>
132160
);
133161
}
134162

163+
class ErrorBoundary extends React.Component<{children?: React$Node}> {
164+
state: {error: any} = {error: null};
165+
static getDerivedStateFromError(error: mixed): {error: any} {
166+
return {error};
167+
}
168+
componentDidCatch(error: any, info: any) {
169+
console.error(error, info);
170+
}
171+
render(): any {
172+
if (this.state.error) {
173+
return <div>Error: {String(this.state.error)}</div>;
174+
}
175+
return this.props.children;
176+
}
177+
}
178+
135179
export default function CustomHooks(): React.Node {
136180
return (
137181
<Fragment>
138182
<FunctionWithHooks />
139183
<MemoWithHooks />
140184
<ForwardRefWithHooks />
141185
<HocWithHooks />
142-
<Forms />
186+
<ErrorBoundary>
187+
<Forms />
188+
</ErrorBoundary>
143189
</Fragment>
144190
);
145191
}

0 commit comments

Comments
 (0)