mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Removes the `enableDispatchCallback` feature flag and deletes the associated code. An earlier version of the Hooks proposal included this feature but we've since decided to remove it.
371 lines
11 KiB
JavaScript
371 lines
11 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its 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 {ReactContext} from 'shared/ReactTypes';
|
|
|
|
import invariant from 'shared/invariant';
|
|
import warning from 'shared/warning';
|
|
|
|
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 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;
|
|
// 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;
|
|
|
|
function resolveCurrentlyRenderingComponent(): Object {
|
|
invariant(
|
|
currentlyRenderingComponent !== null,
|
|
'Hooks can only be called inside the body of a function component.',
|
|
);
|
|
return currentlyRenderingComponent;
|
|
}
|
|
|
|
function createHook(): Hook {
|
|
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(componentIdentity: Object): void {
|
|
currentlyRenderingComponent = componentIdentity;
|
|
|
|
// The following should have already been reset
|
|
// didScheduleRenderPhaseUpdate = false;
|
|
// firstWorkInProgressHook = null;
|
|
// numberOfReRenders = 0;
|
|
// renderPhaseUpdates = null;
|
|
// workInProgressHook = null;
|
|
}
|
|
|
|
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;
|
|
numberOfReRenders += 1;
|
|
|
|
// Start over from the beginning of the list
|
|
workInProgressHook = null;
|
|
|
|
children = Component(props, refOrContext);
|
|
}
|
|
currentlyRenderingComponent = null;
|
|
firstWorkInProgressHook = null;
|
|
numberOfReRenders = 0;
|
|
renderPhaseUpdates = null;
|
|
workInProgressHook = null;
|
|
|
|
// These were reset above
|
|
// currentlyRenderingComponent = null;
|
|
// didScheduleRenderPhaseUpdate = false;
|
|
// firstWorkInProgressHook = null;
|
|
// numberOfReRenders = 0;
|
|
// renderPhaseUpdates = null;
|
|
// workInProgressHook = null;
|
|
|
|
return children;
|
|
}
|
|
|
|
function readContext<T>(
|
|
context: ReactContext<T>,
|
|
observedBits: void | number | boolean,
|
|
): T {
|
|
return context._currentValue;
|
|
}
|
|
|
|
function useContext<T>(
|
|
context: ReactContext<T>,
|
|
observedBits: void | number | boolean,
|
|
): T {
|
|
resolveCurrentlyRenderingComponent();
|
|
return context._currentValue;
|
|
}
|
|
|
|
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>>] {
|
|
return useReducer(
|
|
basicStateReducer,
|
|
// useReducer has a special case to support lazy useState initializers
|
|
(initialState: any),
|
|
);
|
|
}
|
|
|
|
export function useReducer<S, A>(
|
|
reducer: (S, A) => S,
|
|
initialState: S,
|
|
initialAction: A | void | null,
|
|
): [S, Dispatch<A>] {
|
|
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) {
|
|
renderPhaseUpdates.delete(queue);
|
|
let newState = workInProgressHook.memoizedState;
|
|
let update = 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;
|
|
newState = reducer(newState, action);
|
|
update = update.next;
|
|
} while (update !== null);
|
|
|
|
workInProgressHook.memoizedState = newState;
|
|
|
|
return [newState, dispatch];
|
|
}
|
|
}
|
|
return [workInProgressHook.memoizedState, dispatch];
|
|
} else {
|
|
if (reducer === basicStateReducer) {
|
|
// Special case for `useState`.
|
|
if (typeof initialState === 'function') {
|
|
initialState = initialState();
|
|
}
|
|
} else if (initialAction !== undefined && initialAction !== null) {
|
|
initialState = reducer(initialState, initialAction);
|
|
}
|
|
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,
|
|
inputs: Array<mixed> | void | null,
|
|
): T {
|
|
currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
|
|
workInProgressHook = createWorkInProgressHook();
|
|
|
|
const nextInputs =
|
|
inputs !== undefined && inputs !== null ? inputs : [nextCreate];
|
|
|
|
if (
|
|
workInProgressHook !== null &&
|
|
workInProgressHook.memoizedState !== null
|
|
) {
|
|
const prevState = workInProgressHook.memoizedState;
|
|
const prevInputs = prevState[1];
|
|
if (inputsAreEqual(nextInputs, prevInputs)) {
|
|
return prevState[0];
|
|
}
|
|
}
|
|
|
|
const nextValue = nextCreate();
|
|
workInProgressHook.memoizedState = [nextValue, nextInputs];
|
|
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 useMutationEffect(
|
|
create: () => mixed,
|
|
inputs: Array<mixed> | void | null,
|
|
) {
|
|
warning(
|
|
false,
|
|
'useMutationEffect does nothing on the server, because its effect cannot ' +
|
|
"be encoded into the server renderer's output format. This will lead " +
|
|
'to a mismatch between the initial, non-hydrated UI and the intended ' +
|
|
'UI. To avoid this, useMutationEffect should only be used in ' +
|
|
'components that render exclusively on the client.',
|
|
);
|
|
}
|
|
|
|
export function useLayoutEffect(
|
|
create: () => mixed,
|
|
inputs: Array<mixed> | void | null,
|
|
) {
|
|
warning(
|
|
false,
|
|
'useLayoutEffect does nothing on the server, because its effect cannot ' +
|
|
"be encoded into the server renderer's output format. This will lead " +
|
|
'to a mismatch between the initial, non-hydrated UI and the intended ' +
|
|
'UI. To avoid this, useLayoutEffect should only be used in ' +
|
|
'components that render exclusively on the client.',
|
|
);
|
|
}
|
|
|
|
function dispatchAction<A>(
|
|
componentIdentity: Object,
|
|
queue: UpdateQueue<A>,
|
|
action: A,
|
|
) {
|
|
invariant(
|
|
numberOfReRenders < RE_RENDER_LIMIT,
|
|
'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) {
|
|
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.
|
|
}
|
|
}
|
|
|
|
function inputsAreEqual(arr1, arr2) {
|
|
// Don't bother comparing lengths because these arrays are always
|
|
// passed inline.
|
|
for (let i = 0; i < arr1.length; i++) {
|
|
// Inlined Object.is polyfill.
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
|
|
const val1 = arr1[i];
|
|
const val2 = arr2[i];
|
|
if (
|
|
(val1 === val2 && (val1 !== 0 || 1 / val1 === 1 / (val2: any))) ||
|
|
(val1 !== val1 && val2 !== val2) // eslint-disable-line no-self-compare
|
|
) {
|
|
continue;
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function noop(): void {}
|
|
|
|
export const Dispatcher = {
|
|
readContext,
|
|
useContext,
|
|
useMemo,
|
|
useReducer,
|
|
useRef,
|
|
useState,
|
|
useMutationEffect,
|
|
useLayoutEffect,
|
|
// useImperativeMethods is not run in the server environment
|
|
useImperativeMethods: noop,
|
|
// Callbacks are not run in the server environment.
|
|
useCallback: noop,
|
|
// Effects are not run in the server environment.
|
|
useEffect: noop,
|
|
};
|