Files
react/packages/react-server/src/ReactFizzHooks.js
T
Sebastian Markbåge 9f2eebd807 [Fiber/Fizz] Support AsyncIterable as Children and AsyncGenerator Client Components (#28868)
Stacked on #28849, #28854, #28853. Behind a flag.

If you're following along from the side-lines. This is probably not what
you think it is.

It's NOT a way to get updates to a component over time. The
AsyncIterable works like an Iterable already works in React which is how
an Array works. I.e. it's a list of children - not the value of a child
over time.

It also doesn't actually render one component at a time. The way it
works is more like awaiting the entire list to become an array and then
it shows up. Before that it suspends the parent.

To actually get these to display one at a time, you have to opt-in with
`<SuspenseList>` to describe how they should appear. That's really the
interesting part and that not implemented yet.

Additionally, since these are effectively Async Functions and uncached
promises, they're not actually fully "supported" on the client yet for
the same reason rendering plain Promises and Async Functions aren't.
They warn. It's only really useful when paired with RSC that produces
instrumented versions of these. Ideally we'd published instrumented
helpers to help with map/filter style operations that yield new
instrumented AsyncIterables.

The way the implementation works basically just relies on unwrapThenable
and otherwise works like a plain Iterator.

There is one quirk with these that are different than just promises. We
ask for a new iterator each time we rerender. This means that upon retry
we kick off another iteration which itself might kick off new requests
that block iterating further. To solve this and make it actually
efficient enough to use on the client we'd need to stash something like
a buffer of the previous iteration and maybe iterator on the iterable so
that we can continue where we left off or synchronously iterate if we've
seen it before. Similar to our `.value` convention on Promises.

In Fizz, I had to do a special case because when we render an iterator
child we don't actually rerender the parent again like we do in Fiber.
However, it's more efficient to just continue on where we left off by
reusing the entries from the thenable state from before in that case.
2024-04-22 13:25:05 -04:00

854 lines
26 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
import type {
ReactContext,
StartTransitionOptions,
Thenable,
Usable,
ReactCustomFormAction,
Awaited,
} from 'shared/ReactTypes';
import type {ResumableState} from './ReactFizzConfig';
import type {Request, Task, KeyNode} from './ReactFizzServer';
import type {ThenableState} from './ReactFizzThenable';
import type {TransitionStatus} from './ReactFizzConfig';
import {readContext as readContextImpl} from './ReactFizzNewContext';
import {getTreeId} from './ReactFizzTreeContext';
import {
createThenableState,
trackUsedThenable,
readPreviousThenable,
} from './ReactFizzThenable';
import {makeId, NotPendingTransition} from './ReactFizzConfig';
import {createFastHash} from './ReactServerStreamConfig';
import {
enableCache,
enableUseEffectEventHook,
enableUseMemoCacheHook,
enableAsyncActions,
enableUseDeferredValueInitialArg,
} from 'shared/ReactFeatureFlags';
import is from 'shared/objectIs';
import {
REACT_CONTEXT_TYPE,
REACT_MEMO_CACHE_SENTINEL,
} from 'shared/ReactSymbols';
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
import {getFormState} from './ReactFizzServer';
type BasicStateAction<S> = (S => S) | S;
type Dispatch<A> = A => void;
type Update<A> = {
action: A,
next: Update<A> | null,
};
type UpdateQueue<A> = {
last: Update<A> | null,
dispatch: any,
};
type Hook = {
memoizedState: any,
queue: UpdateQueue<any> | null,
next: Hook | null,
};
let currentlyRenderingComponent: Object | null = null;
let currentlyRenderingTask: Task | null = null;
let currentlyRenderingRequest: Request | null = null;
let currentlyRenderingKeyPath: KeyNode | null = null;
let firstWorkInProgressHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
// Whether the work-in-progress hook is a re-rendered hook
let isReRender: boolean = false;
// Whether an update was scheduled during the currently executing render pass.
let didScheduleRenderPhaseUpdate: boolean = false;
// Counts the number of useId hooks in this component
let localIdCounter: number = 0;
// Chunks that should be pushed to the stream once the component
// finishes rendering.
// Counts the number of useActionState calls in this component
let actionStateCounter: number = 0;
// The index of the useActionState hook that matches the one passed in at the
// root during an MPA navigation, if any.
let actionStateMatchingIndex: number = -1;
// Counts the number of use(thenable) calls in this component
let thenableIndexCounter: number = 0;
let thenableState: ThenableState | null = null;
// Lazily created map of render-phase updates
let renderPhaseUpdates: Map<UpdateQueue<any>, Update<any>> | null = null;
// Counter to prevent infinite loops.
let numberOfReRenders: number = 0;
const RE_RENDER_LIMIT = 25;
let isInHookUserCodeInDev = false;
// In DEV, this is the name of the currently executing primitive hook
let currentHookNameInDev: ?string;
function resolveCurrentlyRenderingComponent(): Object {
if (currentlyRenderingComponent === null) {
throw new Error(
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
);
}
if (__DEV__) {
if (isInHookUserCodeInDev) {
console.error(
'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' +
'You can only call Hooks at the top level of your React function. ' +
'For more information, see ' +
'https://react.dev/link/rules-of-hooks',
);
}
}
return currentlyRenderingComponent;
}
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
if (prevDeps === null) {
if (__DEV__) {
console.error(
'%s received a final argument during this render, but not during ' +
'the previous render. Even though the final argument is optional, ' +
'its type cannot change between renders.',
currentHookNameInDev,
);
}
return false;
}
if (__DEV__) {
// Don't bother comparing lengths in prod because these arrays should be
// passed inline.
if (nextDeps.length !== prevDeps.length) {
console.error(
'The final argument passed to %s changed size between renders. The ' +
'order and size of this array must remain constant.\n\n' +
'Previous: %s\n' +
'Incoming: %s',
currentHookNameInDev,
`[${nextDeps.join(', ')}]`,
`[${prevDeps.join(', ')}]`,
);
}
}
// $FlowFixMe[incompatible-use] found when upgrading Flow
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
// $FlowFixMe[incompatible-use] found when upgrading Flow
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
function createHook(): Hook {
if (numberOfReRenders > 0) {
throw new Error('Rendered more hooks than during the previous render');
}
return {
memoizedState: null,
queue: null,
next: null,
};
}
function createWorkInProgressHook(): Hook {
if (workInProgressHook === null) {
// This is the first hook in the list
if (firstWorkInProgressHook === null) {
isReRender = false;
firstWorkInProgressHook = workInProgressHook = createHook();
} else {
// There's already a work-in-progress. Reuse it.
isReRender = true;
workInProgressHook = firstWorkInProgressHook;
}
} else {
if (workInProgressHook.next === null) {
isReRender = false;
// Append to the end of the list
workInProgressHook = workInProgressHook.next = createHook();
} else {
// There's already a work-in-progress. Reuse it.
isReRender = true;
workInProgressHook = workInProgressHook.next;
}
}
return workInProgressHook;
}
export function prepareToUseHooks(
request: Request,
task: Task,
keyPath: KeyNode | null,
componentIdentity: Object,
prevThenableState: ThenableState | null,
): void {
currentlyRenderingComponent = componentIdentity;
currentlyRenderingTask = task;
currentlyRenderingRequest = request;
currentlyRenderingKeyPath = keyPath;
if (__DEV__) {
isInHookUserCodeInDev = false;
}
// The following should have already been reset
// didScheduleRenderPhaseUpdate = false;
// firstWorkInProgressHook = null;
// numberOfReRenders = 0;
// renderPhaseUpdates = null;
// workInProgressHook = null;
localIdCounter = 0;
actionStateCounter = 0;
actionStateMatchingIndex = -1;
thenableIndexCounter = 0;
thenableState = prevThenableState;
}
export function prepareToUseThenableState(
prevThenableState: ThenableState | null,
): void {
thenableIndexCounter = 0;
thenableState = prevThenableState;
}
export function finishHooks(
Component: any,
props: any,
children: any,
refOrContext: any,
): any {
// This must be called after every function component to prevent hooks from
// being used in classes.
while (didScheduleRenderPhaseUpdate) {
// Updates were scheduled during the render phase. They are stored in
// the `renderPhaseUpdates` map. Call the component again, reusing the
// work-in-progress hooks and applying the additional updates on top. Keep
// restarting until no more updates are scheduled.
didScheduleRenderPhaseUpdate = false;
localIdCounter = 0;
actionStateCounter = 0;
actionStateMatchingIndex = -1;
thenableIndexCounter = 0;
numberOfReRenders += 1;
// Start over from the beginning of the list
workInProgressHook = null;
children = Component(props, refOrContext);
}
resetHooksState();
return children;
}
export function getThenableStateAfterSuspending(): null | ThenableState {
const state = thenableState;
thenableState = null;
return state;
}
export function checkDidRenderIdHook(): boolean {
// This should be called immediately after every finishHooks call.
// Conceptually, it's part of the return value of finishHooks; it's only a
// separate function to avoid using an array tuple.
const didRenderIdHook = localIdCounter !== 0;
return didRenderIdHook;
}
export function getActionStateCount(): number {
// This should be called immediately after every finishHooks call.
// Conceptually, it's part of the return value of finishHooks; it's only a
// separate function to avoid using an array tuple.
return actionStateCounter;
}
export function getActionStateMatchingIndex(): number {
// This should be called immediately after every finishHooks call.
// Conceptually, it's part of the return value of finishHooks; it's only a
// separate function to avoid using an array tuple.
return actionStateMatchingIndex;
}
// Reset the internal hooks state if an error occurs while rendering a component
export function resetHooksState(): void {
if (__DEV__) {
isInHookUserCodeInDev = false;
}
currentlyRenderingComponent = null;
currentlyRenderingTask = null;
currentlyRenderingRequest = null;
currentlyRenderingKeyPath = null;
didScheduleRenderPhaseUpdate = false;
firstWorkInProgressHook = null;
numberOfReRenders = 0;
renderPhaseUpdates = null;
workInProgressHook = null;
}
function readContext<T>(context: ReactContext<T>): T {
if (__DEV__) {
if (isInHookUserCodeInDev) {
console.error(
'Context can only be read while React is rendering. ' +
'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
'In function components, you can read it directly in the function body, but not ' +
'inside Hooks like useReducer() or useMemo().',
);
}
}
return readContextImpl(context);
}
function useContext<T>(context: ReactContext<T>): T {
if (__DEV__) {
currentHookNameInDev = 'useContext';
}
resolveCurrentlyRenderingComponent();
return readContextImpl(context);
}
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
// $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
}
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
if (__DEV__) {
currentHookNameInDev = 'useState';
}
return useReducer(
basicStateReducer,
// useReducer has a special case to support lazy useState initializers
(initialState: any),
);
}
export function useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
if (__DEV__) {
if (reducer !== basicStateReducer) {
currentHookNameInDev = 'useReducer';
}
}
currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
workInProgressHook = createWorkInProgressHook();
if (isReRender) {
// This is a re-render. Apply the new render phase updates to the previous
// current hook.
const queue: UpdateQueue<A> = (workInProgressHook.queue: any);
const dispatch: Dispatch<A> = (queue.dispatch: any);
if (renderPhaseUpdates !== null) {
// Render phase updates are stored in a map of queue -> linked list
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate !== undefined) {
// $FlowFixMe[incompatible-use] found when upgrading Flow
renderPhaseUpdates.delete(queue);
// $FlowFixMe[incompatible-use] found when upgrading Flow
let newState = workInProgressHook.memoizedState;
let update: Update<any> = firstRenderPhaseUpdate;
do {
// Process this render phase update. We don't have to check the
// priority because it will always be the same as the current
// render's.
const action = update.action;
if (__DEV__) {
isInHookUserCodeInDev = true;
}
newState = reducer(newState, action);
if (__DEV__) {
isInHookUserCodeInDev = false;
}
// $FlowFixMe[incompatible-type] we bail out when we get a null
update = update.next;
} while (update !== null);
// $FlowFixMe[incompatible-use] found when upgrading Flow
workInProgressHook.memoizedState = newState;
return [newState, dispatch];
}
}
// $FlowFixMe[incompatible-use] found when upgrading Flow
return [workInProgressHook.memoizedState, dispatch];
} else {
if (__DEV__) {
isInHookUserCodeInDev = true;
}
let initialState;
if (reducer === basicStateReducer) {
// Special case for `useState`.
initialState =
typeof initialArg === 'function'
? ((initialArg: any): () => S)()
: ((initialArg: any): S);
} else {
initialState =
init !== undefined ? init(initialArg) : ((initialArg: any): S);
}
if (__DEV__) {
isInHookUserCodeInDev = false;
}
// $FlowFixMe[incompatible-use] found when upgrading Flow
workInProgressHook.memoizedState = initialState;
// $FlowFixMe[incompatible-use] found when upgrading Flow
const queue: UpdateQueue<A> = (workInProgressHook.queue = {
last: null,
dispatch: null,
});
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingComponent,
queue,
): any));
// $FlowFixMe[incompatible-use] found when upgrading Flow
return [workInProgressHook.memoizedState, dispatch];
}
}
function useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
workInProgressHook = createWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
if (workInProgressHook !== null) {
const prevState = workInProgressHook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
}
if (__DEV__) {
isInHookUserCodeInDev = true;
}
const nextValue = nextCreate();
if (__DEV__) {
isInHookUserCodeInDev = false;
}
// $FlowFixMe[incompatible-use] found when upgrading Flow
workInProgressHook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
function useRef<T>(initialValue: T): {current: T} {
currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
workInProgressHook = createWorkInProgressHook();
const previousRef = workInProgressHook.memoizedState;
if (previousRef === null) {
const ref = {current: initialValue};
if (__DEV__) {
Object.seal(ref);
}
// $FlowFixMe[incompatible-use] found when upgrading Flow
workInProgressHook.memoizedState = ref;
return ref;
} else {
return previousRef;
}
}
function dispatchAction<A>(
componentIdentity: Object,
queue: UpdateQueue<A>,
action: A,
): void {
if (numberOfReRenders >= RE_RENDER_LIMIT) {
throw new Error(
'Too many re-renders. React limits the number of renders to prevent ' +
'an infinite loop.',
);
}
if (componentIdentity === currentlyRenderingComponent) {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
didScheduleRenderPhaseUpdate = true;
const update: Update<A> = {
action,
next: null,
};
if (renderPhaseUpdates === null) {
renderPhaseUpdates = new Map();
}
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate === undefined) {
// $FlowFixMe[incompatible-use] found when upgrading Flow
renderPhaseUpdates.set(queue, update);
} else {
// Append the update to the end of the list.
let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
while (lastRenderPhaseUpdate.next !== null) {
lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
}
lastRenderPhaseUpdate.next = update;
}
} else {
// This means an update has happened after the function component has
// returned. On the server this is a no-op. In React Fiber, the update
// would be scheduled for a future render.
}
}
export function useCallback<T>(
callback: T,
deps: Array<mixed> | void | null,
): T {
return useMemo(() => callback, deps);
}
function throwOnUseEffectEventCall() {
throw new Error(
"A function wrapped in useEffectEvent can't be called during rendering.",
);
}
export function useEffectEvent<Args, Return, F: (...Array<Args>) => Return>(
callback: F,
): F {
// $FlowIgnore[incompatible-return]
return throwOnUseEffectEventCall;
}
function useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
if (getServerSnapshot === undefined) {
throw new Error(
'Missing getServerSnapshot, which is required for ' +
'server-rendered content. Will revert to client rendering.',
);
}
return getServerSnapshot();
}
function useDeferredValue<T>(value: T, initialValue?: T): T {
resolveCurrentlyRenderingComponent();
if (enableUseDeferredValueInitialArg) {
return initialValue !== undefined ? initialValue : value;
} else {
return value;
}
}
function unsupportedStartTransition() {
throw new Error('startTransition cannot be called during server rendering.');
}
function useTransition(): [
boolean,
(callback: () => void, options?: StartTransitionOptions) => void,
] {
resolveCurrentlyRenderingComponent();
return [false, unsupportedStartTransition];
}
function useHostTransitionStatus(): TransitionStatus {
resolveCurrentlyRenderingComponent();
return NotPendingTransition;
}
function unsupportedSetOptimisticState() {
throw new Error('Cannot update optimistic state while rendering.');
}
function useOptimistic<S, A>(
passthrough: S,
reducer: ?(S, A) => S,
): [S, (A) => void] {
resolveCurrentlyRenderingComponent();
return [passthrough, unsupportedSetOptimisticState];
}
function createPostbackActionStateKey(
permalink: string | void,
componentKeyPath: KeyNode | null,
hookIndex: number,
): string {
if (permalink !== undefined) {
// Don't bother to hash a permalink-based key since it's already short.
return 'p' + permalink;
} else {
// Append a node to the key path that represents the form state hook.
const keyPath: KeyNode = [componentKeyPath, null, hookIndex];
// Key paths are hashed to reduce the size. It does not need to be secure,
// and it's more important that it's fast than that it's completely
// collision-free.
const keyPathHash = createFastHash(JSON.stringify(keyPath));
return 'k' + keyPathHash;
}
}
function useActionState<S, P>(
action: (Awaited<S>, P) => S,
initialState: Awaited<S>,
permalink?: string,
): [Awaited<S>, (P) => void, boolean] {
resolveCurrentlyRenderingComponent();
// Count the number of useActionState hooks per component. We also use this to
// track the position of this useActionState hook relative to the other ones in
// this component, so we can generate a unique key for each one.
const actionStateHookIndex = actionStateCounter++;
const request: Request = (currentlyRenderingRequest: any);
// $FlowIgnore[prop-missing]
const formAction = action.$$FORM_ACTION;
if (typeof formAction === 'function') {
// This is a server action. These have additional features to enable
// MPA-style form submissions with progressive enhancement.
// TODO: If the same permalink is passed to multiple useActionStates, and
// they all have the same action signature, Fizz will pass the postback
// state to all of them. We should probably only pass it to the first one,
// and/or warn.
// The key is lazily generated and deduped so the that the keypath doesn't
// get JSON.stringify-ed unnecessarily, and at most once.
let nextPostbackStateKey = null;
// Determine the current form state. If we received state during an MPA form
// submission, then we will reuse that, if the action identity matches.
// Otherwise, we'll use the initial state argument. We will emit a comment
// marker into the stream that indicates whether the state was reused.
let state = initialState;
const componentKeyPath = (currentlyRenderingKeyPath: any);
const postbackActionState = getFormState(request);
// $FlowIgnore[prop-missing]
const isSignatureEqual = action.$$IS_SIGNATURE_EQUAL;
if (
postbackActionState !== null &&
typeof isSignatureEqual === 'function'
) {
const postbackKey = postbackActionState[1];
const postbackReferenceId = postbackActionState[2];
const postbackBoundArity = postbackActionState[3];
if (
isSignatureEqual.call(action, postbackReferenceId, postbackBoundArity)
) {
nextPostbackStateKey = createPostbackActionStateKey(
permalink,
componentKeyPath,
actionStateHookIndex,
);
if (postbackKey === nextPostbackStateKey) {
// This was a match
actionStateMatchingIndex = actionStateHookIndex;
// Reuse the state that was submitted by the form.
state = postbackActionState[0];
}
}
}
// Bind the state to the first argument of the action.
const boundAction = action.bind(null, state);
// Wrap the action so the return value is void.
const dispatch = (payload: P): void => {
boundAction(payload);
};
// $FlowIgnore[prop-missing]
if (typeof boundAction.$$FORM_ACTION === 'function') {
// $FlowIgnore[prop-missing]
dispatch.$$FORM_ACTION = (prefix: string) => {
const metadata: ReactCustomFormAction =
boundAction.$$FORM_ACTION(prefix);
// Override the action URL
if (permalink !== undefined) {
if (__DEV__) {
checkAttributeStringCoercion(permalink, 'target');
}
permalink += '';
metadata.action = permalink;
}
const formData = metadata.data;
if (formData) {
if (nextPostbackStateKey === null) {
nextPostbackStateKey = createPostbackActionStateKey(
permalink,
componentKeyPath,
actionStateHookIndex,
);
}
formData.append('$ACTION_KEY', nextPostbackStateKey);
}
return metadata;
};
}
return [state, dispatch, false];
} else {
// This is not a server action, so the implementation is much simpler.
// Bind the state to the first argument of the action.
const boundAction = action.bind(null, initialState);
// Wrap the action so the return value is void.
const dispatch = (payload: P): void => {
boundAction(payload);
};
return [initialState, dispatch, false];
}
}
function useId(): string {
const task: Task = (currentlyRenderingTask: any);
const treeId = getTreeId(task.treeContext);
const resumableState = currentResumableState;
if (resumableState === null) {
throw new Error(
'Invalid hook call. Hooks can only be called inside of the body of a function component.',
);
}
const localId = localIdCounter++;
return makeId(resumableState, treeId, localId);
}
function use<T>(usable: Usable<T>): T {
if (usable !== null && typeof usable === 'object') {
// $FlowFixMe[method-unbinding]
if (typeof usable.then === 'function') {
// This is a thenable.
const thenable: Thenable<T> = (usable: any);
return unwrapThenable(thenable);
} else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
const context: ReactContext<T> = (usable: any);
return readContext(context);
}
}
// eslint-disable-next-line react-internal/safe-string-coercion
throw new Error('An unsupported type was passed to use(): ' + String(usable));
}
export function unwrapThenable<T>(thenable: Thenable<T>): T {
const index = thenableIndexCounter;
thenableIndexCounter += 1;
if (thenableState === null) {
thenableState = createThenableState();
}
return trackUsedThenable(thenableState, thenable, index);
}
export function readPreviousThenableFromState<T>(): T | void {
const index = thenableIndexCounter;
thenableIndexCounter += 1;
if (thenableState === null) {
return undefined;
}
return readPreviousThenable(thenableState, index);
}
function unsupportedRefresh() {
throw new Error('Cache cannot be refreshed during server rendering.');
}
function useCacheRefresh(): <T>(?() => T, ?T) => void {
return unsupportedRefresh;
}
function useMemoCache(size: number): Array<any> {
const data = new Array<any>(size);
for (let i = 0; i < size; i++) {
data[i] = REACT_MEMO_CACHE_SENTINEL;
}
return data;
}
function noop(): void {}
export const HooksDispatcher: Dispatcher = {
readContext,
use,
useContext,
useMemo,
useReducer,
useRef,
useState,
useInsertionEffect: noop,
useLayoutEffect: noop,
useCallback,
// useImperativeHandle is not run in the server environment
useImperativeHandle: noop,
// Effects are not run in the server environment.
useEffect: noop,
// Debugging effect
useDebugValue: noop,
useDeferredValue,
useTransition,
useId,
// Subscriptions are not setup in a server environment.
useSyncExternalStore,
};
if (enableCache) {
HooksDispatcher.useCacheRefresh = useCacheRefresh;
}
if (enableUseEffectEventHook) {
HooksDispatcher.useEffectEvent = useEffectEvent;
}
if (enableUseMemoCacheHook) {
HooksDispatcher.useMemoCache = useMemoCache;
}
if (enableAsyncActions) {
HooksDispatcher.useHostTransitionStatus = useHostTransitionStatus;
}
if (enableAsyncActions) {
HooksDispatcher.useOptimistic = useOptimistic;
HooksDispatcher.useFormState = useActionState;
HooksDispatcher.useActionState = useActionState;
}
export let currentResumableState: null | ResumableState = (null: any);
export function setCurrentResumableState(
resumableState: null | ResumableState,
): void {
currentResumableState = resumableState;
}