Skip to content

Commit

Permalink
Devtools: Unwrap Promise in useFormState (facebook#28319)
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon authored and AndyPengc12 committed Apr 15, 2024
1 parent b317f2e commit 0144744
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 11 deletions.
55 changes: 49 additions & 6 deletions packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -521,19 +521,62 @@ function useFormState<S, P>(
): [Awaited<S>, (P) => void] {
const hook = nextHook(); // FormState
nextHook(); // ActionQueue
let state;
const stackError = new Error();
let value;
let debugInfo = null;
let error = null;

if (hook !== null) {
state = hook.memoizedState;
const actionResult = hook.memoizedState;
if (
typeof actionResult === 'object' &&
actionResult !== null &&
// $FlowFixMe[method-unbinding]
typeof actionResult.then === 'function'
) {
const thenable: Thenable<Awaited<S>> = (actionResult: any);
switch (thenable.status) {
case 'fulfilled': {
value = thenable.value;
debugInfo =
thenable._debugInfo === undefined ? null : thenable._debugInfo;
break;
}
case 'rejected': {
const rejectedError = thenable.reason;
error = rejectedError;
break;
}
default:
// If this was an uncached Promise we have to abandon this attempt
// but we can still emit anything up until this point.
error = SuspenseException;
debugInfo =
thenable._debugInfo === undefined ? null : thenable._debugInfo;
value = thenable;
}
} else {
value = (actionResult: any);
}
} else {
state = initialState;
value = initialState;
}

hookLog.push({
displayName: null,
primitive: 'FormState',
stackError: new Error(),
value: state,
debugInfo: null,
stackError: stackError,
value: value,
debugInfo: debugInfo,
});

if (error !== null) {
throw error;
}

// value being a Thenable is equivalent to error being not null
// i.e. we only reach this point with Awaited<S>
const state = ((value: any): Awaited<S>);
return [state, (payload: P) => {}];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,26 +120,72 @@ function wrapWithHoc(Component: (props: any, ref: React$Ref<any>) => any) {
}
const HocWithHooks = wrapWithHoc(FunctionWithHooks);

function incrementWithDelay(previousState: number, formData: FormData) {
const incrementDelay = +formData.get('incrementDelay');
const shouldReject = formData.get('shouldReject');
const reason = formData.get('reason');

return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldReject) {
reject(reason);
} else {
resolve(previousState + 1);
}
}, incrementDelay);
});
}

function Forms() {
const [state, formAction] = useFormState((n: number, formData: FormData) => {
return n + 1;
}, 0);
const [state, formAction] = useFormState<any, any>(incrementWithDelay, 0);
return (
<form>
{state}
State: {state}&nbsp;
<label>
delay:
<input
name="incrementDelay"
defaultValue={5000}
type="text"
inputMode="numeric"
/>
</label>
<label>
Reject:
<input name="reason" type="text" />
<input name="shouldReject" type="checkbox" />
</label>
<button formAction={formAction}>Increment</button>
</form>
);
}

class ErrorBoundary extends React.Component<{children?: React$Node}> {
state: {error: any} = {error: null};
static getDerivedStateFromError(error: mixed): {error: any} {
return {error};
}
componentDidCatch(error: any, info: any) {
console.error(error, info);
}
render(): any {
if (this.state.error) {
return <div>Error: {String(this.state.error)}</div>;
}
return this.props.children;
}
}

export default function CustomHooks(): React.Node {
return (
<Fragment>
<FunctionWithHooks />
<MemoWithHooks />
<ForwardRefWithHooks />
<HocWithHooks />
<Forms />
<ErrorBoundary>
<Forms />
</ErrorBoundary>
</Fragment>
);
}
Expand Down

0 comments on commit 0144744

Please sign in to comment.