@samitouri / QOS-React-2 / commits / fb10a2c66a

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}&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>
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 }