Devtools: Unwrap Promise in useFormState (#28319)
Sebastian Silbermann committed
Feb 28, 2024 at 23:52 UTC
fb10a2c66a923d218471b535fdaf0dbc530417ee
2 files changed
+100
-11
packages/react-debug-tools/src/ReactDebugHooks.js
+49
-6
@@ -521,19 +521,62 @@ function useFormState<S, P>(
521
): [Awaited<S>, (P) => void] {
522
const hook = nextHook(); // FormState
523
nextHook(); // ActionQueue
524
- let state;
524
+ const stackError = new Error();
525
+ let value;
526
+ let debugInfo = null;
527
+ let error = null;
528
+
529
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
+ }
561
} else {
528
- state = initialState;
562
+ value = initialState;
563
}
564
+
565
hookLog.push({
566
displayName: null,
567
primitive: 'FormState',
533
- stackError: new Error(),
534
- value: state,
535
- debugInfo: null,
568
+ stackError: stackError,
569
+ value: value,
570
+ debugInfo: debugInfo,
571
});
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>);
580
return [state, (payload: P) => {}];
581
}
582
packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js
+51
-5
@@ -120,18 +120,62 @@ function wrapWithHoc(Component: (props: any, ref: React$Ref<any>) => any) {
120
}
121
const HocWithHooks = wrapWithHoc(FunctionWithHooks);
122
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 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);
141
return (
142
<form>
129
- {state}
143
+ State: {state}
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>
158
<button formAction={formAction}>Increment</button>
159
</form>
160
);
161
}
162
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
+
179
export default function CustomHooks(): React.Node {
180
return (
181
<Fragment>
@@ -139,7 +183,9 @@ export default function CustomHooks(): React.Node {
183
<MemoWithHooks />
184
<ForwardRefWithHooks />
185
<HocWithHooks />
142
- <Forms />
186
+ <ErrorBoundary>
187
+ <Forms />
188
+ </ErrorBoundary>
189
</Fragment>
190
);
191
}