Skip to content

Commit

Permalink
WIP DevTools: Add support for useFormStatus
Browse files Browse the repository at this point in the history
  • Loading branch information
Sebastian Silbermann committed Feb 21, 2024
1 parent edf0d35 commit 26474b8
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 8 deletions.
47 changes: 46 additions & 1 deletion packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
Fiber,
Dispatcher as DispatcherType,
} from 'react-reconciler/src/ReactInternalTypes';
import type {TransitionStatus} from 'react-reconciler/src/ReactFiberConfig';

import ErrorStackParser from 'error-stack-parser';
import assign from 'shared/assign';
Expand All @@ -29,6 +30,7 @@ import {
SimpleMemoComponent,
ContextProvider,
ForwardRef,
HostComponent,
} from 'react-reconciler/src/ReactWorkTags';
import {
REACT_MEMO_CACHE_SENTINEL,
Expand Down Expand Up @@ -117,6 +119,11 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
);
} catch (x) {}
}

if (typeof Dispatcher.useHostTransitionStatus === 'function') {
// This type check is for Flow only.
Dispatcher.useHostTransitionStatus();
}
} finally {
readHookLog = hookLog;
hookLog = [];
Expand Down Expand Up @@ -509,6 +516,40 @@ function useFormState<S, P>(
return [state, (payload: P) => {}];
}

function useHostTransitionStatus(): TransitionStatus {
// The type is host specific. This is just a placeholder.
let status: mixed = null;

if (currentFiber !== null) {
const dependencies = currentFiber.dependencies;
if (dependencies !== null) {
let contextDependency = dependencies.firstContext;
while (contextDependency !== null) {
if (
// $FlowFixMe -- TODO
contextDependency.context.$$contextType ===
Symbol.for('react.HostTransitionContext')
) {
// TODO: This as well as contextDependency.context.currentValue is null because setupContext does not set the value yet
status = contextDependency.memoizedValue;
}
contextDependency = contextDependency.next;
}
}
}

// TODO: Handle host transition context not found.

hookLog.push({
primitive: 'HostTransitionStatus',
stackError: new Error(),
value: status,
debugInfo: null,
});

return ((status: any): TransitionStatus);
}

const Dispatcher: DispatcherType = {
use,
readContext,
Expand All @@ -531,6 +572,7 @@ const Dispatcher: DispatcherType = {
useDeferredValue,
useId,
useFormState,
useHostTransitionStatus,
};

// create a proxy to throw a custom error
Expand Down Expand Up @@ -787,7 +829,8 @@ function buildTree(
primitive === 'Context (use)' ||
primitive === 'DebugValue' ||
primitive === 'Promise' ||
primitive === 'Unresolved'
primitive === 'Unresolved' ||
primitive === 'HostTransitionStatus'
? null
: nativeHookID++;

Expand Down Expand Up @@ -939,6 +982,8 @@ function setupContexts(contextMap: Map<ReactContext<any>, any>, fiber: Fiber) {
// Set the inner most provider value on the context.
context._currentValue = current.memoizedProps.value;
}
} else if (current.tag === HostComponent) {
// TODO: Set the HostTransitionContext value here?
}
current = current.return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1332,4 +1332,54 @@ describe('ReactHooksInspectionIntegration', () => {
},
]);
});

// @gate enableFormActions && enableAsyncActions
it('should support useFormStatus hook', () => {
function Foo() {
const status = ReactDOM.useFormStatus();
React.useMemo(() => 'memo', []);
React.useMemo(() => 'not used', []);

return JSON.stringify(status);
}

const renderer = ReactTestRenderer.create(<Foo />);
const childFiber = renderer.root.findByType(Foo)._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([
{
debugInfo: null,
id: null,
isStateEditable: false,
name: '.useFormStatus',
subHooks: [
{
debugInfo: null,
id: null,
isStateEditable: false,
name: 'HostTransitionStatus',
subHooks: [],
value: null,
},
],
value: null,
},
{
debugInfo: null,
id: 0,
isStateEditable: false,
name: 'Memo',
subHooks: [],
value: 'memo',
},
{
debugInfo: null,
id: 1,
isStateEditable: false,
name: 'Memo',
subHooks: [],
value: 'not used',
},
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
useState,
use,
} from 'react';
import {useFormState} from 'react-dom';
import {useFormState, useFormStatus} from 'react-dom';

const object = {
string: 'abc',
Expand Down Expand Up @@ -120,26 +120,79 @@ 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 FormStatus() {
const status = useFormStatus();

return JSON.stringify(status);
}

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>
<FormStatus />
</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
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ export function useFormStatus(): FormStatus {
} else {
const dispatcher = resolveDispatcher();
// $FlowFixMe[not-a-function] We know this exists because of the feature check above.
return dispatcher.useHostTransitionStatus();
const status = dispatcher.useHostTransitionStatus();

dispatcher.useDebugValue(status);

return status;
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/react-reconciler/src/ReactFiberHostContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ export const HostTransitionContext: ReactContext<TransitionStatus | null> = {
Consumer: (null: any),
};

// $FlowFixMe -- TODO
HostTransitionContext.$$contextType = Symbol.for('react.HostTransitionContext');

function requiredContext<Value>(c: Value | null): Value {
if (__DEV__) {
if (c === null) {
Expand Down

0 comments on commit 26474b8

Please sign in to comment.