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,
supportsClientAPIs,
} from './ReactFizzConfig';
import {createFastHash} from './ReactServerStreamConfig';
import {enableUseEffectEventHook} 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';
import noop from 'shared/noop';
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;
let isReRender: boolean = false;
let didScheduleRenderPhaseUpdate: boolean = false;
let localIdCounter: number = 0;
let actionStateCounter: number = 0;
let actionStateMatchingIndex: number = -1;
let thenableIndexCounter: number = 0;
let thenableState: ThenableState | null = null;
let renderPhaseUpdates: Map<UpdateQueue<any>, Update<any>> | null = null;
let numberOfReRenders: number = 0;
const RE_RENDER_LIMIT = 25;
let isInHookUserCodeInDev = false;
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__) {
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(', ')}]`,
);
}
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
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) {
if (firstWorkInProgressHook === null) {
isReRender = false;
firstWorkInProgressHook = workInProgressHook = createHook();
} else {
isReRender = true;
workInProgressHook = firstWorkInProgressHook;
}
} else {
if (workInProgressHook.next === null) {
isReRender = false;
workInProgressHook = workInProgressHook.next = createHook();
} else {
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;
}
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 {
while (didScheduleRenderPhaseUpdate) {
didScheduleRenderPhaseUpdate = false;
localIdCounter = 0;
actionStateCounter = 0;
actionStateMatchingIndex = -1;
thenableIndexCounter = 0;
numberOfReRenders += 1;
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 {
const didRenderIdHook = localIdCounter !== 0;
return didRenderIdHook;
}
export function getActionStateCount(): number {
return actionStateCounter;
}
export function getActionStateMatchingIndex(): number {
return actionStateMatchingIndex;
}
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 {
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,
(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) {
const queue: UpdateQueue<A> = (workInProgressHook.queue: any);
const dispatch: Dispatch<A> = (queue.dispatch: any);
if (renderPhaseUpdates !== null) {
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate !== undefined) {
renderPhaseUpdates.delete(queue);
let newState = workInProgressHook.memoizedState;
let update: Update<any> = firstRenderPhaseUpdate;
do {
const action = update.action;
if (__DEV__) {
isInHookUserCodeInDev = true;
}
newState = reducer(newState, action);
if (__DEV__) {
isInHookUserCodeInDev = false;
}
update = update.next;
} while (update !== null);
workInProgressHook.memoizedState = newState;
return [newState, dispatch];
}
}
return [workInProgressHook.memoizedState, dispatch];
} else {
if (__DEV__) {
isInHookUserCodeInDev = true;
}
let initialState;
if (reducer === basicStateReducer) {
initialState =
typeof initialArg === 'function'
? ((initialArg: any): () => S)()
: ((initialArg: any): S);
} else {
initialState =
init !== undefined ? init(initialArg) : ((initialArg: any): S);
}
if (__DEV__) {
isInHookUserCodeInDev = false;
}
workInProgressHook.memoizedState = initialState;
const queue: UpdateQueue<A> = (workInProgressHook.queue = {
last: null,
dispatch: null,
});
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingComponent,
queue,
): any));
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;
}
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);
}
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) {
didScheduleRenderPhaseUpdate = true;
const update: Update<A> = {
action,
next: null,
};
if (renderPhaseUpdates === null) {
renderPhaseUpdates = new Map();
}
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate === undefined) {
renderPhaseUpdates.set(queue, update);
} else {
let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
while (lastRenderPhaseUpdate.next !== null) {
lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
}
lastRenderPhaseUpdate.next = update;
}
} else {
}
}
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 {
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();
return initialValue !== undefined ? initialValue : 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) {
return 'p' + permalink;
} else {
const keyPath: KeyNode = [componentKeyPath, null, hookIndex];
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();
const actionStateHookIndex = actionStateCounter++;
const request: Request = (currentlyRenderingRequest: any);
const formAction = action.$$FORM_ACTION;
if (typeof formAction === 'function') {
let nextPostbackStateKey = null;
let state = initialState;
const componentKeyPath = (currentlyRenderingKeyPath: any);
const postbackActionState = getFormState(request);
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) {
actionStateMatchingIndex = actionStateHookIndex;
state = postbackActionState[0];
}
}
}
const boundAction = action.bind(null, state);
const dispatch = (payload: P): void => {
boundAction(payload);
};
if (typeof boundAction.$$FORM_ACTION === 'function') {
dispatch.$$FORM_ACTION = (prefix: string) => {
const metadata: ReactCustomFormAction =
boundAction.$$FORM_ACTION(prefix);
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 {
const boundAction = action.bind(null, initialState);
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') {
if (typeof usable.then === 'function') {
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);
}
}
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<mixed> {
const data = new Array<any>(size);
for (let i = 0; i < size; i++) {
data[i] = REACT_MEMO_CACHE_SENTINEL;
}
return data;
}
function clientHookNotSupported() {
throw new Error(
'Cannot use state or effect Hooks in renderToHTML because ' +
'this component will never be hydrated.',
);
}
export const HooksDispatcher: Dispatcher = supportsClientAPIs
? {
readContext,
use,
useContext,
useMemo,
useReducer,
useRef,
useState,
useInsertionEffect: noop,
useLayoutEffect: noop,
useCallback,
useImperativeHandle: noop,
useEffect: noop,
useDebugValue: noop,
useDeferredValue,
useTransition,
useId,
useSyncExternalStore,
useOptimistic,
useActionState,
useFormState: useActionState,
useHostTransitionStatus,
useMemoCache,
useCacheRefresh,
}
: {
readContext,
use,
useCallback,
useContext,
useEffect: clientHookNotSupported,
useImperativeHandle: clientHookNotSupported,
useInsertionEffect: clientHookNotSupported,
useLayoutEffect: clientHookNotSupported,
useMemo,
useReducer: clientHookNotSupported,
useRef: clientHookNotSupported,
useState: clientHookNotSupported,
useDebugValue: noop,
useDeferredValue: clientHookNotSupported,
useTransition: clientHookNotSupported,
useSyncExternalStore: clientHookNotSupported,
useId,
useHostTransitionStatus,
useFormState: useActionState,
useActionState,
useOptimistic,
useMemoCache,
useCacheRefresh,
};
if (enableUseEffectEventHook) {
HooksDispatcher.useEffectEvent = useEffectEvent;
}
export let currentResumableState: null | ResumableState = (null: any);
export function setCurrentResumableState(
resumableState: null | ResumableState,
): void {
currentResumableState = resumableState;
}