Skip to content

Commit

Permalink
Initial hooks implementation
Browse files Browse the repository at this point in the history
Includes:
- useState
- useContext
- useEffect
- useRef
- useReducer
- useCallback
- useMemo
- useAPI
  • Loading branch information
acdlite committed Oct 29, 2018
1 parent 37c7fe0 commit 7bee9fb
Show file tree
Hide file tree
Showing 9 changed files with 2,075 additions and 31 deletions.
31 changes: 16 additions & 15 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Expand Up @@ -79,6 +79,7 @@ import {
prepareToReadContext,
calculateChangedBits,
} from './ReactFiberNewContext';
import {prepareToUseHooks, finishHooks, resetHooks} from './ReactFiberHooks';
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer';
import {
getMaskedContext,
Expand Down Expand Up @@ -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');
Expand All @@ -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,
Expand Down Expand Up @@ -406,6 +400,7 @@ function updateFunctionComponent(

let nextChildren;
prepareToReadContext(workInProgress, renderExpirationTime);
prepareToUseHooks(current, workInProgress, renderExpirationTime);
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
ReactCurrentFiber.setCurrentPhase('render');
Expand All @@ -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;
Expand Down Expand Up @@ -921,6 +917,7 @@ function mountIndeterminateComponent(
const context = getMaskedContext(workInProgress, unmaskedContext);

prepareToReadContext(workInProgress, renderExpirationTime);
prepareToUseHooks(null, workInProgress, renderExpirationTime);

let value;

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down
170 changes: 170 additions & 0 deletions packages/react-reconciler/src/ReactFiberCommitWork.js
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -235,13 +254,145 @@ 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,
finishedWork: Fiber,
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) {
Expand Down Expand Up @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions packages/react-reconciler/src/ReactFiberDispatcher.js
Expand Up @@ -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,
};

0 comments on commit 7bee9fb

Please sign in to comment.