Initial hooks implementation

Includes:
- useState
- useContext
- useEffect
- useRef
- useReducer
- useCallback
- useMemo
- useAPI
This commit is contained in:
Andrew Clark
2018-09-05 11:29:08 -07:00
committed by Andrew Clark
parent 37c7fe0a5f
commit 7bee9fbdd4
9 changed files with 2075 additions and 31 deletions

View File

@@ -79,6 +79,7 @@ import {
prepareToReadContext,
calculateChangedBits,
} from './ReactFiberNewContext';
import {prepareToUseHooks, finishHooks, resetHooks} from './ReactFiberHooks';
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer';
import {
getMaskedContext,
@@ -193,27 +194,17 @@ function forceUnmountCurrentAndReconcile(
function updateForwardRef(
current: Fiber | null,
workInProgress: Fiber,
type: any,
Component: any,
nextProps: any,
renderExpirationTime: ExpirationTime,
) {
const render = type.render;
const render = Component.render;
const ref = workInProgress.ref;
if (hasLegacyContextChanged()) {
// Normally we can bail out on props equality but if context has changed
// we don't do the bailout and we have to reuse existing props instead.
} else if (workInProgress.memoizedProps === nextProps) {
const currentRef = current !== null ? current.ref : null;
if (ref === currentRef) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
}
// The rest is a fork of updateFunctionComponent
let nextChildren;
prepareToReadContext(workInProgress, renderExpirationTime);
prepareToUseHooks(current, workInProgress, renderExpirationTime);
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
ReactCurrentFiber.setCurrentPhase('render');
@@ -222,7 +213,10 @@ function updateForwardRef(
} else {
nextChildren = render(nextProps, ref);
}
nextChildren = finishHooks(render, nextProps, nextChildren, ref);
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
reconcileChildren(
current,
workInProgress,
@@ -406,6 +400,7 @@ function updateFunctionComponent(
let nextChildren;
prepareToReadContext(workInProgress, renderExpirationTime);
prepareToUseHooks(current, workInProgress, renderExpirationTime);
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
ReactCurrentFiber.setCurrentPhase('render');
@@ -414,6 +409,7 @@ function updateFunctionComponent(
} else {
nextChildren = Component(nextProps, context);
}
nextChildren = finishHooks(Component, nextProps, nextChildren, context);
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
@@ -921,6 +917,7 @@ function mountIndeterminateComponent(
const context = getMaskedContext(workInProgress, unmaskedContext);
prepareToReadContext(workInProgress, renderExpirationTime);
prepareToUseHooks(null, workInProgress, renderExpirationTime);
let value;
@@ -964,6 +961,9 @@ function mountIndeterminateComponent(
// Proceed under the assumption that this is a class instance
workInProgress.tag = ClassComponent;
// Throw out any hooks that were used.
resetHooks();
// Push context providers early to prevent context stack mismatches.
// During mounting we don't know the child context yet as the instance doesn't exist.
// We will invalidate the child context in finishClassComponent() right after rendering.
@@ -1001,6 +1001,7 @@ function mountIndeterminateComponent(
} else {
// Proceed under the assumption that this is a function component
workInProgress.tag = FunctionComponent;
value = finishHooks(Component, props, value, context);
if (__DEV__) {
if (Component) {
warningWithoutStack(

View File

@@ -19,12 +19,15 @@ import type {FiberRoot} from './ReactFiberRoot';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {CapturedValue, CapturedError} from './ReactCapturedValue';
import type {SuspenseState} from './ReactFiberSuspenseComponent';
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks';
import {
enableSchedulerTracing,
enableProfilerTimer,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
ForwardRef,
ClassComponent,
HostRoot,
HostComponent,
@@ -180,6 +183,22 @@ function safelyDetachRef(current: Fiber) {
}
}
function safelyCallDestroy(current, destroy) {
if (__DEV__) {
invokeGuardedCallback(null, destroy, null);
if (hasCaughtError()) {
const error = clearCaughtError();
captureCommitPhaseError(current, error);
}
} else {
try {
destroy();
} catch (error) {
captureCommitPhaseError(current, error);
}
}
}
function commitBeforeMutationLifeCycles(
current: Fiber | null,
finishedWork: Fiber,
@@ -235,6 +254,28 @@ function commitBeforeMutationLifeCycles(
}
}
function destroyRemainingEffects(firstToDestroy, stopAt) {
let effect = firstToDestroy;
do {
const destroy = effect.value;
if (destroy !== null) {
destroy();
}
effect = effect.next;
} while (effect !== stopAt);
}
function destroyMountedEffects(current) {
const oldUpdateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
if (oldUpdateQueue !== null) {
const oldLastEffect = oldUpdateQueue.lastEffect;
if (oldLastEffect !== null) {
const oldFirstEffect = oldLastEffect.next;
destroyRemainingEffects(oldFirstEffect, oldFirstEffect);
}
}
}
function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
@@ -242,6 +283,116 @@ function commitLifeCycles(
committedExpirationTime: ExpirationTime,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef: {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
if (updateQueue !== null) {
// Mount new effects and destroy the old ones by comparing to the
// current list of effects. This could be a bit simpler if we avoided
// the need to compare to the previous effect list by transferring the
// old `destroy` method to the new effect during the render phase.
// That's how I originally implemented it, but it requires an additional
// field on the effect object.
//
// This supports removing effects from the end of the list. If we adopt
// the constraint that hooks are append only, that would also save a bit
// on code size.
const newLastEffect = updateQueue.lastEffect;
if (newLastEffect !== null) {
const newFirstEffect = newLastEffect.next;
let oldLastEffect = null;
if (current !== null) {
const oldUpdateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
if (oldUpdateQueue !== null) {
oldLastEffect = oldUpdateQueue.lastEffect;
}
}
if (oldLastEffect !== null) {
const oldFirstEffect = oldLastEffect.next;
let newEffect = newFirstEffect;
let oldEffect = oldFirstEffect;
// Before mounting the new effects, unmount all the old ones.
do {
if (oldEffect !== null) {
if (newEffect.inputs !== oldEffect.inputs) {
const destroy = oldEffect.value;
if (destroy !== null) {
destroy();
}
}
oldEffect = oldEffect.next;
if (oldEffect === oldFirstEffect) {
oldEffect = null;
}
}
newEffect = newEffect.next;
} while (newEffect !== newFirstEffect);
// Unmount any remaining effects in the old list that do not
// appear in the new one.
if (oldEffect !== null) {
destroyRemainingEffects(oldEffect, oldFirstEffect);
}
// Now loop through the list again to mount the new effects
oldEffect = oldFirstEffect;
do {
const create = newEffect.value;
if (oldEffect !== null) {
if (newEffect.inputs !== oldEffect.inputs) {
const newDestroy = create();
newEffect.value =
typeof newDestroy === 'function' ? newDestroy : null;
} else {
newEffect.value = oldEffect.value;
}
oldEffect = oldEffect.next;
if (oldEffect === oldFirstEffect) {
oldEffect = null;
}
} else {
const newDestroy = create();
newEffect.value =
typeof newDestroy === 'function' ? newDestroy : null;
}
newEffect = newEffect.next;
} while (newEffect !== newFirstEffect);
} else {
let newEffect = newFirstEffect;
do {
const create = newEffect.value;
const newDestroy = create();
newEffect.value =
typeof newDestroy === 'function' ? newDestroy : null;
newEffect = newEffect.next;
} while (newEffect !== newFirstEffect);
}
} else if (current !== null) {
// There are no effects, which means all current effects must
// be destroyed
destroyMountedEffects(current);
}
const callbackList = updateQueue.callbackList;
if (callbackList !== null) {
updateQueue.callbackList = null;
for (let i = 0; i < callbackList.length; i++) {
const update = callbackList[i];
// Assume this is non-null, since otherwise it would not be part
// of the callback list.
const callback: () => mixed = (update.callback: any);
update.callback = null;
callback();
}
}
} else if (current !== null) {
// There are no effects, which means all current effects must
// be destroyed
destroyMountedEffects(current);
}
break;
}
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.effectTag & Update) {
@@ -496,6 +647,25 @@ function commitUnmount(current: Fiber): void {
onCommitUnmount(current);
switch (current.tag) {
case FunctionComponent:
case ForwardRef: {
const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
if (updateQueue !== null) {
const lastEffect = updateQueue.lastEffect;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const destroy = effect.value;
if (destroy !== null) {
safelyCallDestroy(current, destroy);
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
break;
}
case ClassComponent: {
safelyDetachRef(current);
const instance = current.stateNode;

View File

@@ -8,7 +8,23 @@
*/
import {readContext} from './ReactFiberNewContext';
import {
useState,
useReducer,
useEffect,
useCallback,
useMemo,
useRef,
useAPI,
} from './ReactFiberHooks';
export const Dispatcher = {
readContext,
useState,
useReducer,
useEffect,
useCallback,
useMemo,
useRef,
useAPI,
};

View File

@@ -0,0 +1,691 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root direcreatey of this source tree.
*
* @flow
*/
import type {Fiber} from './ReactFiber';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import {NoWork} from './ReactFiberExpirationTime';
import {Callback as CallbackEffect} from 'shared/ReactSideEffectTags';
import {
scheduleWork,
computeExpirationForFiber,
requestCurrentTime,
} from './ReactFiberScheduler';
import invariant from 'shared/invariant';
type Update<S, A> = {
expirationTime: ExpirationTime,
action: A,
callback: null | (S => mixed),
next: Update<S, A> | null,
};
type UpdateQueue<S, A> = {
last: Update<S, A> | null,
dispatch: any,
};
type Hook = {
memoizedState: any,
baseState: any,
baseUpdate: Update<any, any> | null,
queue: UpdateQueue<any, any> | null,
next: Hook | null,
};
type Effect = {
// For an unmounted effect, this points to the effect constructor. Once it's
// mounted, it points to a destroy function (or null). I've opted to reuse
// the same field to save memory.
value: any,
inputs: Array<mixed>,
next: Effect,
};
export type FunctionComponentUpdateQueue = {
callbackList: Array<Update<any, any>> | null,
lastEffect: Effect | null,
};
type BasicStateAction<S> = S | (S => S);
type MaybeCallback<S> = void | null | (S => mixed);
type Dispatch<S, A> = (A, MaybeCallback<S>) => void;
// These are set right before calling the component.
let renderExpirationTime: ExpirationTime = NoWork;
// The work-in-progress fiber. I've named it differently to distinguish it from
// the work-in-progress hook.
let currentlyRenderingFiber: Fiber | null = null;
// Hooks are stored as a linked list on the fiber's memoizedState field. The
// current hook list is the list that belongs to the current fiber. The
// work-in-progress hook list is a new list that will be added to the
// work-in-progress fiber.
let firstCurrentHook: Hook | null = null;
let currentHook: Hook | null = null;
let firstWorkInProgressHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
let remainingExpirationTime: ExpirationTime = NoWork;
let componentUpdateQueue: FunctionComponentUpdateQueue | null = null;
// Updates scheduled during render will trigger an immediate re-render at the
// end of the current pass. We can't store these updates on the normal queue,
// because if the work is aborted, they should be discarded. Because this is
// a relatively rare case, we also don't want to add an additional field to
// either the hook or queue object types. So we store them in a lazily create
// map of queue -> render-phase updates, which are discarded once the component
// completes without re-rendering.
// 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, any>,
Update<any, any>,
> | null = null;
// Counter to prevent infinite loops.
let numberOfReRenders: number = 0;
const RE_RENDER_LIMIT = 25;
function resolveCurrentlyRenderingFiber(): Fiber {
invariant(
currentlyRenderingFiber !== null,
'Hooks can only be called inside the body of a functional component.',
);
return currentlyRenderingFiber;
}
export function prepareToUseHooks(
current: Fiber | null,
workInProgress: Fiber,
nextRenderExpirationTime: ExpirationTime,
): void {
renderExpirationTime = nextRenderExpirationTime;
currentlyRenderingFiber = workInProgress;
firstCurrentHook = current !== null ? current.memoizedState : null;
// The following should have already been reset
// currentHook = null;
// workInProgressHook = null;
// remainingExpirationTime = NoWork;
// componentUpdateQueue = null;
// isReRender = false;
// didScheduleRenderPhaseUpdate = false;
// renderPhaseUpdates = null;
// numberOfReRenders = 0;
}
export function finishHooks(
Component: any,
props: any,
children: any,
refOrContext: any,
): any {
// This must be called after every functional 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
currentHook = null;
workInProgressHook = null;
componentUpdateQueue = null;
children = Component(props, refOrContext);
}
renderPhaseUpdates = null;
numberOfReRenders = 0;
const renderedWork: Fiber = (currentlyRenderingFiber: any);
renderedWork.memoizedState = firstWorkInProgressHook;
renderedWork.expirationTime = remainingExpirationTime;
if (componentUpdateQueue !== null) {
renderedWork.updateQueue = (componentUpdateQueue: any);
}
renderExpirationTime = NoWork;
currentlyRenderingFiber = null;
firstCurrentHook = null;
currentHook = null;
firstWorkInProgressHook = null;
workInProgressHook = null;
remainingExpirationTime = NoWork;
componentUpdateQueue = null;
// Always set during createWorkInProgress
// isReRender = false;
// These were reset above
// didScheduleRenderPhaseUpdate = false;
// renderPhaseUpdates = null;
// numberOfReRenders = 0;
return children;
}
export function resetHooks(): void {
// This is called instead of `finishHooks` if the component throws. It's also
// called inside mountIndeterminateComponent if we determine the component
// is a module-style component.
renderExpirationTime = NoWork;
currentlyRenderingFiber = null;
firstCurrentHook = null;
currentHook = null;
firstWorkInProgressHook = null;
workInProgressHook = null;
remainingExpirationTime = NoWork;
componentUpdateQueue = null;
// Always set during createWorkInProgress
// isReRender = false;
didScheduleRenderPhaseUpdate = false;
renderPhaseUpdates = null;
numberOfReRenders = 0;
}
function createHook(): Hook {
return {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
}
function cloneHook(hook: Hook): Hook {
return {
memoizedState: hook.memoizedState,
baseState: hook.memoizedState,
queue: hook.queue,
baseUpdate: hook.baseUpdate,
next: null,
};
}
function createWorkInProgressHook(): Hook {
if (workInProgressHook === null) {
// This is the first hook in the list
if (firstWorkInProgressHook === null) {
isReRender = false;
currentHook = firstCurrentHook;
if (currentHook === null) {
// This is a newly mounted hook
workInProgressHook = createHook();
} else {
// Clone the current hook.
workInProgressHook = cloneHook(currentHook);
}
firstWorkInProgressHook = workInProgressHook;
} else {
// There's already a work-in-progress. Reuse it.
isReRender = true;
currentHook = firstCurrentHook;
workInProgressHook = firstWorkInProgressHook;
}
} else {
if (workInProgressHook.next === null) {
isReRender = false;
let hook;
if (currentHook === null) {
// This is a newly mounted hook
hook = createHook();
} else {
currentHook = currentHook.next;
if (currentHook === null) {
// This is a newly mounted hook
hook = createHook();
} else {
// Clone the current hook.
hook = cloneHook(currentHook);
}
}
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
} else {
// There's already a work-in-progress. Reuse it.
isReRender = true;
workInProgressHook = workInProgressHook.next;
currentHook = currentHook !== null ? currentHook.next : null;
}
}
return workInProgressHook;
}
function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
return {
callbackList: null,
lastEffect: null,
};
}
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<S, 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<S, A>] {
currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
workInProgressHook = createWorkInProgressHook();
if (isReRender) {
// This is a re-render. Apply the new render phase updates to the previous
// work-in-progress hook.
const queue: UpdateQueue<S, A> = (workInProgressHook.queue: any);
const dispatch: Dispatch<S, 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);
const callback = update.callback;
if (callback !== null) {
pushCallback(currentlyRenderingFiber, update);
}
update = update.next;
} while (update !== null);
workInProgressHook.memoizedState = newState;
// Don't persist the state accumlated from the render phase updates to
// the base state unless the queue is empty.
// TODO: Not sure if this is the desired semantics, but it's what we
// do for gDSFP. I can't remember why.
if (workInProgressHook.baseUpdate === queue.last) {
workInProgressHook.baseState = newState;
}
return [newState, dispatch];
}
}
return [workInProgressHook.memoizedState, dispatch];
} else if (currentHook !== null) {
const queue: UpdateQueue<S, A> = (workInProgressHook.queue: any);
// The last update in the entire queue
const last = queue.last;
// The last update that is part of the base state.
const baseUpdate = workInProgressHook.baseUpdate;
// Find the first unprocessed update.
let first;
if (baseUpdate !== null) {
if (last !== null) {
// For the first update, the queue is a circular linked list where
// `queue.last.next = queue.first`. Once the first update commits, and
// the `baseUpdate` is no longer empty, we can unravel the list.
last.next = null;
}
first = baseUpdate.next;
} else {
first = last !== null ? last.next : null;
}
if (first !== null) {
let newState = workInProgressHook.baseState;
let newBaseState = null;
let newBaseUpdate = null;
let prevUpdate = baseUpdate;
let update = first;
let didSkip = false;
do {
const updateExpirationTime = update.expirationTime;
if (updateExpirationTime > renderExpirationTime) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
if (!didSkip) {
didSkip = true;
newBaseUpdate = prevUpdate;
newBaseState = newState;
}
// Update the remaining priority in the queue.
if (
remainingExpirationTime === NoWork ||
updateExpirationTime < remainingExpirationTime
) {
remainingExpirationTime = updateExpirationTime;
}
} else {
// Process this update.
const action = update.action;
newState = reducer(newState, action);
const callback = update.callback;
if (callback !== null) {
pushCallback(currentlyRenderingFiber, update);
}
}
prevUpdate = update;
update = update.next;
} while (update !== null && update !== first);
if (!didSkip) {
newBaseUpdate = prevUpdate;
newBaseState = newState;
}
workInProgressHook.memoizedState = newState;
workInProgressHook.baseUpdate = newBaseUpdate;
workInProgressHook.baseState = newBaseState;
}
const dispatch: Dispatch<S, A> = (queue.dispatch: any);
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 = workInProgressHook.baseState = initialState;
const queue: UpdateQueue<S, A> = (workInProgressHook.queue = {
last: null,
dispatch: null,
});
const dispatch: Dispatch<S, A> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [workInProgressHook.memoizedState, dispatch];
}
}
function pushCallback(workInProgress: Fiber, update: Update<any, any>): void {
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
componentUpdateQueue.callbackList = [update];
} else {
const callbackList = componentUpdateQueue.callbackList;
if (callbackList === null) {
componentUpdateQueue.callbackList = [update];
} else {
callbackList.push(update);
}
}
workInProgress.effectTag |= CallbackEffect;
}
function pushEffect(value, inputs) {
const effect: Effect = {
value,
inputs,
// Circular
next: (null: any),
};
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
export function useRef<T>(initialValue: T): {current: T} {
currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
workInProgressHook = createWorkInProgressHook();
let ref;
if (currentHook === null) {
ref = {current: initialValue};
if (__DEV__) {
Object.seal(ref);
}
workInProgressHook.memoizedState = ref;
} else {
ref = workInProgressHook.memoizedState;
}
return ref;
}
export function useEffect(
create: () => mixed,
inputs: Array<mixed> | void | null,
): void {
currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
workInProgressHook = createWorkInProgressHook();
let nextEffect;
let nextInputs = inputs !== undefined && inputs !== null ? inputs : [create];
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
const prevInputs = prevEffect.inputs;
if (inputsAreEqual(nextInputs, prevInputs)) {
nextEffect = pushEffect(prevEffect.value, prevInputs);
} else {
nextEffect = pushEffect(create, nextInputs);
}
} else {
nextEffect = pushEffect(create, nextInputs);
}
// TODO: If we decide not to support removing hooks from the end of the list,
// we only need to schedule an effect if the inputs changed.
currentlyRenderingFiber.effectTag |= CallbackEffect;
workInProgressHook.memoizedState = nextEffect;
}
export function useAPI<T>(
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
create: () => T,
inputs: Array<mixed> | void | null,
): void {
// TODO: If inputs are provided, should we skip comparing the ref itself?
const nextInputs =
inputs !== null && inputs !== undefined
? inputs.concat([ref])
: [ref, create];
// TODO: I've implemented this on top of useEffect because it's almost the
// same thing, and it would require an equal amount of code. It doesn't seem
// like a common enough use case to justify the additional size.
useEffect(() => {
if (typeof ref === 'function') {
const refCallback = ref;
const inst = create();
refCallback(inst);
return () => refCallback(null);
} else if (ref !== null && ref !== undefined) {
const refObject = ref;
const inst = create();
refObject.current = inst;
return () => {
refObject.current = null;
};
}
}, nextInputs);
}
export function useCallback<T>(
callback: T,
inputs: Array<mixed> | void | null,
): T {
currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
workInProgressHook = createWorkInProgressHook();
const nextInputs =
inputs !== undefined && inputs !== null ? inputs : [callback];
if (currentHook !== null) {
const prevState = currentHook.memoizedState;
const prevInputs = prevState[1];
if (inputsAreEqual(nextInputs, prevInputs)) {
return prevState[0];
}
}
workInProgressHook.memoizedState = [callback, nextInputs];
return callback;
}
export function useMemo<T>(
nextCreate: () => T,
inputs: Array<mixed> | void | null,
): T {
currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
workInProgressHook = createWorkInProgressHook();
const nextInputs =
inputs !== undefined && inputs !== null ? inputs : [nextCreate];
if (currentHook !== null) {
const prevState = currentHook.memoizedState;
const prevInputs = prevState[1];
if (inputsAreEqual(nextInputs, prevInputs)) {
return prevState[0];
}
}
const nextValue = nextCreate();
workInProgressHook.memoizedState = [nextValue, nextInputs];
return nextValue;
}
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
callback: void | null | (S => mixed),
) {
invariant(
numberOfReRenders < RE_RENDER_LIMIT,
'Too many re-renders. React limits the number of renders to prevent ' +
'an infinite loop.',
);
const alternate = fiber.alternate;
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
// 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<S, A> = {
expirationTime: renderExpirationTime,
action,
callback: callback !== undefined ? callback : null,
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 {
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const update: Update<S, A> = {
expirationTime,
action,
callback: callback !== undefined ? callback : null,
next: null,
};
// Append the update to the end of the list.
const last = queue.last;
if (last === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
const first = last.next;
if (first !== null) {
// Still circular.
update.next = first;
}
last.next = update;
}
queue.last = update;
scheduleWork(fiber, expirationTime);
}
}
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;
}

View File

@@ -122,6 +122,7 @@ import {
popContext as popLegacyContext,
} from './ReactFiberContext';
import {popProvider, resetContextDependences} from './ReactFiberNewContext';
import {resetHooks} from './ReactFiberHooks';
import {popHostContext, popHostContainer} from './ReactFiberHostContext';
import {
recordCommitTime,
@@ -1222,6 +1223,9 @@ function renderRoot(
try {
workLoop(isYieldy);
} catch (thrownValue) {
resetContextDependences();
resetHooks();
if (nextUnitOfWork === null) {
// This is a fatal error.
didFatal = true;
@@ -1284,6 +1288,7 @@ function renderRoot(
isWorking = false;
ReactCurrentOwner.currentDispatcher = null;
resetContextDependences();
resetHooks();
// Yield back to main thread.
if (didFatal) {

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@
let ReactFeatureFlags = require('shared/ReactFeatureFlags');
let React = require('react');
let useContext;
let ReactNoop;
let gen;
@@ -21,6 +22,7 @@ describe('ReactNewContext', () => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
React = require('react');
useContext = React.useContext;
ReactNoop = require('react-noop-renderer');
gen = require('random-seed');
});
@@ -34,33 +36,26 @@ describe('ReactNewContext', () => {
return {type: 'span', children: [], prop, hidden: false};
}
function readContext(Context, observedBits) {
const dispatcher =
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner
.currentDispatcher;
return dispatcher.readContext(Context, observedBits);
}
// We have several ways of reading from context. sharedContextTests runs
// a suite of tests for a given context consumer implementation.
sharedContextTests('Context.Consumer', Context => Context.Consumer);
sharedContextTests(
'readContext(Context) inside function component',
'useContext inside functional component',
Context =>
function Consumer(props) {
const observedBits = props.unstable_observedBits;
const contextValue = readContext(Context, observedBits);
const contextValue = useContext(Context, observedBits);
const render = props.children;
return render(contextValue);
},
);
sharedContextTests(
'readContext(Context) inside class component',
'useContext inside class component',
Context =>
class Consumer extends React.Component {
render() {
const observedBits = this.props.unstable_observedBits;
const contextValue = readContext(Context, observedBits);
const contextValue = useContext(Context, observedBits);
const render = this.props.children;
return render(contextValue);
}
@@ -1194,7 +1189,7 @@ describe('ReactNewContext', () => {
return (
<FooContext.Consumer>
{foo => {
const bar = readContext(BarContext);
const bar = useContext(BarContext);
return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
}}
</FooContext.Consumer>
@@ -1238,7 +1233,7 @@ describe('ReactNewContext', () => {
});
});
describe('readContext', () => {
describe('useContext', () => {
it('can use the same context multiple times in the same function', () => {
const Context = React.createContext({foo: 0, bar: 0, baz: 0}, (a, b) => {
let result = 0;
@@ -1264,13 +1259,13 @@ describe('ReactNewContext', () => {
}
function FooAndBar() {
const {foo} = readContext(Context, 0b001);
const {bar} = readContext(Context, 0b010);
const {foo} = useContext(Context, 0b001);
const {bar} = useContext(Context, 0b010);
return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
}
function Baz() {
const {baz} = readContext(Context, 0b100);
const {baz} = useContext(Context, 0b100);
return <Text text={'Baz: ' + baz} />;
}

View File

@@ -27,6 +27,16 @@ import {createContext} from './ReactContext';
import {lazy} from './ReactLazy';
import forwardRef from './forwardRef';
import memo from './memo';
import {
useContext,
useState,
useReducer,
useRef,
useEffect,
useCallback,
useMemo,
useAPI,
} from './ReactHooks';
import {
createElementWithValidation,
createFactoryWithValidation,
@@ -53,6 +63,15 @@ const React = {
lazy,
memo,
useContext,
useState,
useReducer,
useRef,
useEffect,
useCallback,
useMemo,
useAPI,
Fragment: REACT_FRAGMENT_TYPE,
StrictMode: REACT_STRICT_MODE_TYPE,
Suspense: REACT_SUSPENSE_TYPE,

View File

@@ -0,0 +1,83 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* 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 ReactCurrentOwner from './ReactCurrentOwner';
function resolveDispatcher() {
const dispatcher = ReactCurrentOwner.currentDispatcher;
invariant(
dispatcher !== null,
'Hooks can only be called inside the body of a functional component.',
);
return dispatcher;
}
export function useContext<T>(
Context: ReactContext<T>,
observedBits: number | boolean | void,
) {
const dispatcher = resolveDispatcher();
return dispatcher.readContext(Context, observedBits);
}
export function useState<S>(initialState: S | (() => S)) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
export function useReducer<S, A>(
reducer: (S, A) => S,
initialState: S,
initialAction: A | void | null,
) {
const dispatcher = resolveDispatcher();
return dispatcher.useReducer(reducer, initialState, initialAction);
}
export function useRef<T>(initialValue: T): {current: T} {
const dispatcher = resolveDispatcher();
return dispatcher.useRef(initialValue);
}
export function useEffect(
create: () => mixed,
inputs: Array<mixed> | void | null,
) {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, inputs);
}
export function useCallback(
callback: () => mixed,
inputs: Array<mixed> | void | null,
) {
const dispatcher = resolveDispatcher();
return dispatcher.useCallback(callback, inputs);
}
export function useMemo(
create: () => mixed,
inputs: Array<mixed> | void | null,
) {
const dispatcher = resolveDispatcher();
return dispatcher.useMemo(create, inputs);
}
export function useAPI<T>(
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
create: () => T,
inputs: Array<mixed> | void | null,
): void {
const dispatcher = resolveDispatcher();
return dispatcher.useAPI(ref, create, inputs);
}