Files
react/packages/react-reconciler/src/ReactFiberBeginWork.js
T
Sebastian MarkbågeandGitHub 7268d97d2b Centralize props memoization (#13900)
* Move memoizedProps to after beginWork remove memoizeProps helper

We always call this at the end. This is now enforced to line up since
we do the equality check in the beginning of beginWork. So we can't
have special cases.

* Inline the one caller of memoizeState
2018-10-19 20:12:02 -07:00

1611 lines
50 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 {ReactProviderType, ReactContext} from 'shared/ReactTypes';
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import type {FiberRoot} from './ReactFiberRoot';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {SuspenseState} from './ReactFiberSuspenseComponent';
import checkPropTypes from 'prop-types/checkPropTypes';
import {
IndeterminateComponent,
FunctionComponent,
FunctionComponentLazy,
ClassComponent,
ClassComponentLazy,
HostRoot,
HostComponent,
HostText,
HostPortal,
ForwardRef,
ForwardRefLazy,
Fragment,
Mode,
ContextProvider,
ContextConsumer,
Profiler,
SuspenseComponent,
PureComponent,
PureComponentLazy,
} from 'shared/ReactWorkTags';
import {
NoEffect,
PerformedWork,
Placement,
ContentReset,
DidCapture,
Update,
Ref,
} from 'shared/ReactSideEffectTags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
debugRenderPhaseSideEffects,
debugRenderPhaseSideEffectsForStrictMode,
enableProfilerTimer,
} from 'shared/ReactFeatureFlags';
import invariant from 'shared/invariant';
import shallowEqual from 'shared/shallowEqual';
import getComponentName from 'shared/getComponentName';
import ReactStrictModeWarnings from './ReactStrictModeWarnings';
import warning from 'shared/warning';
import warningWithoutStack from 'shared/warningWithoutStack';
import * as ReactCurrentFiber from './ReactCurrentFiber';
import {startWorkTimer, cancelWorkTimer} from './ReactDebugFiberPerf';
import {
mountChildFibers,
reconcileChildFibers,
cloneChildFibers,
} from './ReactChildFiber';
import {processUpdateQueue} from './ReactUpdateQueue';
import {NoWork, Never} from './ReactFiberExpirationTime';
import {ConcurrentMode, StrictMode} from './ReactTypeOfMode';
import {
shouldSetTextContent,
shouldDeprioritizeSubtree,
} from './ReactFiberHostConfig';
import {pushHostContext, pushHostContainer} from './ReactFiberHostContext';
import {
pushProvider,
propagateContextChange,
readContext,
prepareToReadContext,
calculateChangedBits,
} from './ReactFiberNewContext';
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer';
import {
getMaskedContext,
getUnmaskedContext,
hasContextChanged as hasLegacyContextChanged,
pushContextProvider as pushLegacyContextProvider,
isContextProvider as isLegacyContextProvider,
pushTopLevelContextObject,
invalidateContextProvider,
} from './ReactFiberContext';
import {
enterHydrationState,
resetHydrationState,
tryToClaimNextHydratableInstance,
} from './ReactFiberHydrationContext';
import {
adoptClassInstance,
applyDerivedStateFromProps,
constructClassInstance,
mountClassInstance,
resumeMountClassInstance,
updateClassInstance,
} from './ReactFiberClassComponent';
import {readLazyComponentType} from './ReactFiberLazyComponent';
import {getResultFromResolvedLazyComponent} from 'shared/ReactLazyComponent';
import {
resolveLazyComponentTag,
createFiberFromFragment,
createWorkInProgress,
} from './ReactFiber';
import {REACT_LAZY_TYPE} from 'shared/ReactSymbols';
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
let didWarnAboutBadClass;
let didWarnAboutContextTypeOnFunctionComponent;
let didWarnAboutGetDerivedStateOnFunctionComponent;
let didWarnAboutFunctionRefs;
if (__DEV__) {
didWarnAboutBadClass = {};
didWarnAboutContextTypeOnFunctionComponent = {};
didWarnAboutGetDerivedStateOnFunctionComponent = {};
didWarnAboutFunctionRefs = {};
}
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderExpirationTime: ExpirationTime,
) {
if (current === null) {
// If this is a fresh new component that hasn't been rendered yet, we
// won't update its child set by applying minimal side-effects. Instead,
// we will add them all to the child before it gets rendered. That means
// we can optimize this reconciliation pass by not tracking side-effects.
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
} else {
// If the current child is the same as the work in progress, it means that
// we haven't yet started any work on these children. Therefore, we use
// the clone algorithm to create a copy of all the current children.
// If we had any progressed work already, that is invalid at this point so
// let's throw it out.
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderExpirationTime,
);
}
}
function forceUnmountCurrentAndReconcile(
current: Fiber,
workInProgress: Fiber,
nextChildren: any,
renderExpirationTime: ExpirationTime,
) {
// This function is fork of reconcileChildren. It's used in cases where we
// want to reconcile without matching against the existing set. This has the
// effect of all current children being unmounted; even if the type and key
// are the same, the old child is unmounted and a new child is created.
//
// To do this, we're going to go through the reconcile algorithm twice. In
// the first pass, we schedule a deletion for all the current children by
// passing null.
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
null,
renderExpirationTime,
);
// In the second pass, we mount the new children. The trick here is that we
// pass null in place of where we usually pass the current child set. This has
// the effect of remounting all children regardless of whether their their
// identity matches.
workInProgress.child = reconcileChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
}
function updateForwardRef(
current: Fiber | null,
workInProgress: Fiber,
type: any,
nextProps: any,
renderExpirationTime: ExpirationTime,
) {
const render = type.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,
);
}
}
let nextChildren;
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
ReactCurrentFiber.setCurrentPhase('render');
nextChildren = render(nextProps, ref);
ReactCurrentFiber.setCurrentPhase(null);
} else {
nextChildren = render(nextProps, ref);
}
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
return workInProgress.child;
}
function updatePureComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
updateExpirationTime,
renderExpirationTime: ExpirationTime,
) {
const render = Component.render;
if (
current !== null &&
(updateExpirationTime === NoWork ||
updateExpirationTime > renderExpirationTime)
) {
const prevProps = current.memoizedProps;
// Default to shallow comparison
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps)) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
}
// The rest is a fork of updateFunctionComponent
let nextChildren;
prepareToReadContext(workInProgress, renderExpirationTime);
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
ReactCurrentFiber.setCurrentPhase('render');
nextChildren = render(nextProps);
ReactCurrentFiber.setCurrentPhase(null);
} else {
nextChildren = render(nextProps);
}
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
return workInProgress.child;
}
function updateFragment(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
) {
const nextChildren = workInProgress.pendingProps;
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
return workInProgress.child;
}
function updateMode(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
) {
const nextChildren = workInProgress.pendingProps.children;
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
return workInProgress.child;
}
function updateProfiler(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
) {
if (enableProfilerTimer) {
workInProgress.effectTag |= Update;
}
const nextProps = workInProgress.pendingProps;
const nextChildren = nextProps.children;
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
return workInProgress.child;
}
function markRef(current: Fiber | null, workInProgress: Fiber) {
const ref = workInProgress.ref;
if (
(current === null && ref !== null) ||
(current !== null && current.ref !== ref)
) {
// Schedule a Ref effect
workInProgress.effectTag |= Ref;
}
}
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps: any,
renderExpirationTime,
) {
const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
const context = getMaskedContext(workInProgress, unmaskedContext);
let nextChildren;
prepareToReadContext(workInProgress, renderExpirationTime);
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
ReactCurrentFiber.setCurrentPhase('render');
nextChildren = Component(nextProps, context);
ReactCurrentFiber.setCurrentPhase(null);
} else {
nextChildren = Component(nextProps, context);
}
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
return workInProgress.child;
}
function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps,
renderExpirationTime: ExpirationTime,
) {
// 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.
let hasContext;
if (isLegacyContextProvider(Component)) {
hasContext = true;
pushLegacyContextProvider(workInProgress);
} else {
hasContext = false;
}
prepareToReadContext(workInProgress, renderExpirationTime);
const instance = workInProgress.stateNode;
let shouldUpdate;
if (instance === null) {
if (current !== null) {
// An class component without an instance only mounts if it suspended
// inside a non- concurrent tree, in an inconsistent state. We want to
// tree it like a new mount, even though an empty version of it already
// committed. Disconnect the alternate pointers.
current.alternate = null;
workInProgress.alternate = null;
// Since this is conceptually a new fiber, schedule a Placement effect
workInProgress.effectTag |= Placement;
}
// In the initial pass we might need to construct the instance.
constructClassInstance(
workInProgress,
Component,
nextProps,
renderExpirationTime,
);
mountClassInstance(
workInProgress,
Component,
nextProps,
renderExpirationTime,
);
shouldUpdate = true;
} else if (current === null) {
// In a resume, we'll already have an instance we can reuse.
shouldUpdate = resumeMountClassInstance(
workInProgress,
Component,
nextProps,
renderExpirationTime,
);
} else {
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderExpirationTime,
);
}
return finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderExpirationTime,
);
}
function finishClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
shouldUpdate: boolean,
hasContext: boolean,
renderExpirationTime: ExpirationTime,
) {
// Refs should update even if shouldComponentUpdate returns false
markRef(current, workInProgress);
const didCaptureError = (workInProgress.effectTag & DidCapture) !== NoEffect;
if (!shouldUpdate && !didCaptureError) {
// Context providers should defer to sCU for rendering
if (hasContext) {
invalidateContextProvider(workInProgress, Component, false);
}
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
const instance = workInProgress.stateNode;
// Rerender
ReactCurrentOwner.current = workInProgress;
let nextChildren;
if (
didCaptureError &&
typeof Component.getDerivedStateFromError !== 'function'
) {
// If we captured an error, but getDerivedStateFrom catch is not defined,
// unmount all the children. componentDidCatch will schedule an update to
// re-render a fallback. This is temporary until we migrate everyone to
// the new API.
// TODO: Warn in a future release.
nextChildren = null;
if (enableProfilerTimer) {
stopProfilerTimerIfRunning(workInProgress);
}
} else {
if (__DEV__) {
ReactCurrentFiber.setCurrentPhase('render');
nextChildren = instance.render();
if (
debugRenderPhaseSideEffects ||
(debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictMode)
) {
instance.render();
}
ReactCurrentFiber.setCurrentPhase(null);
} else {
nextChildren = instance.render();
}
}
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
if (current !== null && didCaptureError) {
// If we're recovering from an error, reconcile without reusing any of
// the existing children. Conceptually, the normal children and the children
// that are shown on error are two different sets, so we shouldn't reuse
// normal children even if their identities match.
forceUnmountCurrentAndReconcile(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
} else {
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
}
// Memoize state using the values we just used to render.
// TODO: Restructure so we never read values from the instance.
workInProgress.memoizedState = instance.state;
// The context might have changed so we need to recalculate it.
if (hasContext) {
invalidateContextProvider(workInProgress, Component, true);
}
return workInProgress.child;
}
function pushHostRootContext(workInProgress) {
const root = (workInProgress.stateNode: FiberRoot);
if (root.pendingContext) {
pushTopLevelContextObject(
workInProgress,
root.pendingContext,
root.pendingContext !== root.context,
);
} else if (root.context) {
// Should always be set
pushTopLevelContextObject(workInProgress, root.context, false);
}
pushHostContainer(workInProgress, root.containerInfo);
}
function updateHostRoot(current, workInProgress, renderExpirationTime) {
pushHostRootContext(workInProgress);
const updateQueue = workInProgress.updateQueue;
invariant(
updateQueue !== null,
'If the root does not have an updateQueue, we should have already ' +
'bailed out. This error is likely caused by a bug in React. Please ' +
'file an issue.',
);
const nextProps = workInProgress.pendingProps;
const prevState = workInProgress.memoizedState;
const prevChildren = prevState !== null ? prevState.element : null;
processUpdateQueue(
workInProgress,
updateQueue,
nextProps,
null,
renderExpirationTime,
);
const nextState = workInProgress.memoizedState;
// Caution: React DevTools currently depends on this property
// being called "element".
const nextChildren = nextState.element;
if (nextChildren === prevChildren) {
// If the state is the same as before, that's a bailout because we had
// no work that expires at this time.
resetHydrationState();
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
const root: FiberRoot = workInProgress.stateNode;
if (
(current === null || current.child === null) &&
root.hydrate &&
enterHydrationState(workInProgress)
) {
// If we don't have any current children this might be the first pass.
// We always try to hydrate. If this isn't a hydration pass there won't
// be any children to hydrate which is effectively the same thing as
// not hydrating.
// This is a bit of a hack. We track the host root as a placement to
// know that we're currently in a mounting state. That way isMounted
// works as expected. We must reset this before committing.
// TODO: Delete this when we delete isMounted and findDOMNode.
workInProgress.effectTag |= Placement;
// Ensure that children mount into this root without tracking
// side-effects. This ensures that we don't store Placement effects on
// nodes that will be hydrated.
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
} else {
// Otherwise reset hydration state in case we aborted and resumed another
// root.
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
resetHydrationState();
}
return workInProgress.child;
}
function updateHostComponent(current, workInProgress, renderExpirationTime) {
pushHostContext(workInProgress);
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
}
const type = workInProgress.type;
const nextProps = workInProgress.pendingProps;
const prevProps = current !== null ? current.memoizedProps : null;
let nextChildren = nextProps.children;
const isDirectTextChild = shouldSetTextContent(type, nextProps);
if (isDirectTextChild) {
// We special case a direct text child of a host node. This is a common
// case. We won't handle it as a reified child. We will instead handle
// this in the host environment that also have access to this prop. That
// avoids allocating another HostText fiber and traversing it.
nextChildren = null;
} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
// If we're switching from a direct text child to a normal child, or to
// empty, we need to schedule the text content to be reset.
workInProgress.effectTag |= ContentReset;
}
markRef(current, workInProgress);
// Check the host config to see if the children are offscreen/hidden.
if (
renderExpirationTime !== Never &&
workInProgress.mode & ConcurrentMode &&
shouldDeprioritizeSubtree(type, nextProps)
) {
// Schedule this fiber to re-render at offscreen priority. Then bailout.
workInProgress.expirationTime = Never;
return null;
}
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
return workInProgress.child;
}
function updateHostText(current, workInProgress) {
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
}
// Nothing to do here. This is terminal. We'll do the completion step
// immediately after.
return null;
}
function resolveDefaultProps(Component, baseProps) {
if (Component && Component.defaultProps) {
// Resolve default props. Taken from ReactElement
const props = Object.assign({}, baseProps);
const defaultProps = Component.defaultProps;
for (let propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
return props;
}
return baseProps;
}
function mountIndeterminateComponent(
_current,
workInProgress,
Component,
updateExpirationTime,
renderExpirationTime,
) {
if (_current !== null) {
// An indeterminate component only mounts if it suspended inside a non-
// concurrent tree, in an inconsistent state. We want to tree it like
// a new mount, even though an empty version of it already committed.
// Disconnect the alternate pointers.
_current.alternate = null;
workInProgress.alternate = null;
// Since this is conceptually a new fiber, schedule a Placement effect
workInProgress.effectTag |= Placement;
}
const props = workInProgress.pendingProps;
if (
typeof Component === 'object' &&
Component !== null &&
Component.$$typeof === REACT_LAZY_TYPE
) {
// We can't start a User Timing measurement with correct label yet.
// Cancel and resume right after we know the tag.
cancelWorkTimer(workInProgress);
Component = readLazyComponentType(Component);
const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(
workInProgress,
Component,
));
startWorkTimer(workInProgress);
const resolvedProps = resolveDefaultProps(Component, props);
let child;
switch (resolvedTag) {
case FunctionComponentLazy: {
child = updateFunctionComponent(
null,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
break;
}
case ClassComponentLazy: {
child = updateClassComponent(
null,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
break;
}
case ForwardRefLazy: {
child = updateForwardRef(
null,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
break;
}
case PureComponentLazy: {
child = updatePureComponent(
null,
workInProgress,
Component,
resolvedProps,
updateExpirationTime,
renderExpirationTime,
);
break;
}
default: {
// This message intentionally doesn't metion ForwardRef or PureComponent
// because the fact that it's a separate type of work is an
// implementation detail.
invariant(
false,
'Element type is invalid. Received a promise that resolves to: %s. ' +
'Promise elements must resolve to a class or function.',
Component,
);
}
}
return child;
}
const unmaskedContext = getUnmaskedContext(workInProgress, Component, false);
const context = getMaskedContext(workInProgress, unmaskedContext);
prepareToReadContext(workInProgress, renderExpirationTime);
let value;
if (__DEV__) {
if (
Component.prototype &&
typeof Component.prototype.render === 'function'
) {
const componentName = getComponentName(Component) || 'Unknown';
if (!didWarnAboutBadClass[componentName]) {
warningWithoutStack(
false,
"The <%s /> component appears to have a render method, but doesn't extend React.Component. " +
'This is likely to cause errors. Change %s to extend React.Component instead.',
componentName,
componentName,
);
didWarnAboutBadClass[componentName] = true;
}
}
if (workInProgress.mode & StrictMode) {
ReactStrictModeWarnings.recordLegacyContextWarning(workInProgress, null);
}
ReactCurrentOwner.current = workInProgress;
value = Component(props, context);
} else {
value = Component(props, context);
}
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
if (
typeof value === 'object' &&
value !== null &&
typeof value.render === 'function' &&
value.$$typeof === undefined
) {
// Proceed under the assumption that this is a class instance
workInProgress.tag = ClassComponent;
// 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.
let hasContext = false;
if (isLegacyContextProvider(Component)) {
hasContext = true;
pushLegacyContextProvider(workInProgress);
} else {
hasContext = false;
}
workInProgress.memoizedState =
value.state !== null && value.state !== undefined ? value.state : null;
const getDerivedStateFromProps = Component.getDerivedStateFromProps;
if (typeof getDerivedStateFromProps === 'function') {
applyDerivedStateFromProps(
workInProgress,
Component,
getDerivedStateFromProps,
props,
);
}
adoptClassInstance(workInProgress, value);
mountClassInstance(workInProgress, Component, props, renderExpirationTime);
return finishClassComponent(
null,
workInProgress,
Component,
true,
hasContext,
renderExpirationTime,
);
} else {
// Proceed under the assumption that this is a function component
workInProgress.tag = FunctionComponent;
if (__DEV__) {
if (Component) {
warningWithoutStack(
!Component.childContextTypes,
'%s(...): childContextTypes cannot be defined on a function component.',
Component.displayName || Component.name || 'Component',
);
}
if (workInProgress.ref !== null) {
let info = '';
const ownerName = ReactCurrentFiber.getCurrentFiberOwnerNameInDevOrNull();
if (ownerName) {
info += '\n\nCheck the render method of `' + ownerName + '`.';
}
let warningKey = ownerName || workInProgress._debugID || '';
const debugSource = workInProgress._debugSource;
if (debugSource) {
warningKey = debugSource.fileName + ':' + debugSource.lineNumber;
}
if (!didWarnAboutFunctionRefs[warningKey]) {
didWarnAboutFunctionRefs[warningKey] = true;
warning(
false,
'Function components cannot be given refs. ' +
'Attempts to access this ref will fail.%s',
info,
);
}
}
if (typeof Component.getDerivedStateFromProps === 'function') {
const componentName = getComponentName(Component) || 'Unknown';
if (!didWarnAboutGetDerivedStateOnFunctionComponent[componentName]) {
warningWithoutStack(
false,
'%s: Function components do not support getDerivedStateFromProps.',
componentName,
);
didWarnAboutGetDerivedStateOnFunctionComponent[componentName] = true;
}
}
if (
typeof Component.contextType === 'object' &&
Component.contextType !== null
) {
const componentName = getComponentName(Component) || 'Unknown';
if (!didWarnAboutContextTypeOnFunctionComponent[componentName]) {
warningWithoutStack(
false,
'%s: Function components do not support contextType.',
componentName,
);
didWarnAboutContextTypeOnFunctionComponent[componentName] = true;
}
}
}
reconcileChildren(null, workInProgress, value, renderExpirationTime);
return workInProgress.child;
}
}
function updateSuspenseComponent(
current,
workInProgress,
renderExpirationTime,
) {
const mode = workInProgress.mode;
const nextProps = workInProgress.pendingProps;
// We should attempt to render the primary children unless this boundary
// already suspended during this render (`alreadyCaptured` is true).
let nextState: SuspenseState | null = workInProgress.memoizedState;
let nextDidTimeout;
if (nextState === null) {
// An empty suspense state means this boundary has not yet timed out.
nextDidTimeout = false;
} else {
if (!nextState.alreadyCaptured) {
// Since we haven't already suspended during this commit, clear the
// existing suspense state. We'll try rendering again.
nextDidTimeout = false;
nextState = null;
} else {
// Something in this boundary's subtree already suspended. Switch to
// rendering the fallback children. Set `alreadyCaptured` to true.
nextDidTimeout = true;
if (current !== null && nextState === current.memoizedState) {
// Create a new suspense state to avoid mutating the current tree's.
nextState = {
alreadyCaptured: true,
didTimeout: true,
timedOutAt: nextState.timedOutAt,
};
} else {
// Already have a clone, so it's safe to mutate.
nextState.alreadyCaptured = true;
nextState.didTimeout = true;
}
}
}
// This next part is a bit confusing. If the children timeout, we switch to
// showing the fallback children in place of the "primary" children.
// However, we don't want to delete the primary children because then their
// state will be lost (both the React state and the host state, e.g.
// uncontrolled form inputs). Instead we keep them mounted and hide them.
// Both the fallback children AND the primary children are rendered at the
// same time. Once the primary children are un-suspended, we can delete
// the fallback children — don't need to preserve their state.
//
// The two sets of children are siblings in the host environment, but
// semantically, for purposes of reconciliation, they are two separate sets.
// So we store them using two fragment fibers.
//
// However, we want to avoid allocating extra fibers for every placeholder.
// They're only necessary when the children time out, because that's the
// only time when both sets are mounted.
//
// So, the extra fragment fibers are only used if the children time out.
// Otherwise, we render the primary children directly. This requires some
// custom reconciliation logic to preserve the state of the primary
// children. It's essentially a very basic form of re-parenting.
// `child` points to the child fiber. In the normal case, this is the first
// fiber of the primary children set. In the timed-out case, it's a
// a fragment fiber containing the primary children.
let child;
// `next` points to the next fiber React should render. In the normal case,
// it's the same as `child`: the first fiber of the primary children set.
// In the timed-out case, it's a fragment fiber containing the *fallback*
// children -- we skip over the primary children entirely.
let next;
if (current === null) {
// This is the initial mount. This branch is pretty simple because there's
// no previous state that needs to be preserved.
if (nextDidTimeout) {
// Mount separate fragments for primary and fallback children.
const nextFallbackChildren = nextProps.fallback;
const primaryChildFragment = createFiberFromFragment(
null,
mode,
NoWork,
null,
);
const fallbackChildFragment = createFiberFromFragment(
nextFallbackChildren,
mode,
renderExpirationTime,
null,
);
primaryChildFragment.sibling = fallbackChildFragment;
child = primaryChildFragment;
// Skip the primary children, and continue working on the
// fallback children.
next = fallbackChildFragment;
child.return = next.return = workInProgress;
} else {
// Mount the primary children without an intermediate fragment fiber.
const nextPrimaryChildren = nextProps.children;
child = next = mountChildFibers(
workInProgress,
null,
nextPrimaryChildren,
renderExpirationTime,
);
}
} else {
// This is an update. This branch is more complicated because we need to
// ensure the state of the primary children is preserved.
const prevState = current.memoizedState;
const prevDidTimeout = prevState !== null && prevState.didTimeout;
if (prevDidTimeout) {
// The current tree already timed out. That means each child set is
// wrapped in a fragment fiber.
const currentPrimaryChildFragment: Fiber = (current.child: any);
const currentFallbackChildFragment: Fiber = (currentPrimaryChildFragment.sibling: any);
if (nextDidTimeout) {
// Still timed out. Reuse the current primary children by cloning
// its fragment. We're going to skip over these entirely.
const nextFallbackChildren = nextProps.fallback;
const primaryChildFragment = createWorkInProgress(
currentFallbackChildFragment,
currentFallbackChildFragment.pendingProps,
NoWork,
);
primaryChildFragment.effectTag |= Placement;
// Clone the fallback child fragment, too. These we'll continue
// working on.
const fallbackChildFragment = (primaryChildFragment.sibling = createWorkInProgress(
currentFallbackChildFragment,
nextFallbackChildren,
currentFallbackChildFragment.expirationTime,
));
fallbackChildFragment.effectTag |= Placement;
child = primaryChildFragment;
// Skip the primary children, and continue working on the
// fallback children.
next = fallbackChildFragment;
child.return = next.return = workInProgress;
} else {
// No longer suspended. Switch back to showing the primary children,
// and remove the intermediate fragment fiber.
const nextPrimaryChildren = nextProps.children;
const currentPrimaryChild = currentPrimaryChildFragment.child;
const currentFallbackChild = currentFallbackChildFragment.child;
const primaryChild = reconcileChildFibers(
workInProgress,
currentPrimaryChild,
nextPrimaryChildren,
renderExpirationTime,
);
// Delete the fallback children.
reconcileChildFibers(
workInProgress,
currentFallbackChild,
null,
renderExpirationTime,
);
// Continue rendering the children, like we normally do.
child = next = primaryChild;
}
} else {
// The current tree has not already timed out. That means the primary
// children are not wrapped in a fragment fiber.
const currentPrimaryChild: Fiber = (current.child: any);
if (nextDidTimeout) {
// Timed out. Wrap the children in a fragment fiber to keep them
// separate from the fallback children.
const nextFallbackChildren = nextProps.fallback;
const primaryChildFragment = createFiberFromFragment(
// It shouldn't matter what the pending props are because we aren't
// going to render this fragment.
null,
mode,
NoWork,
null,
);
primaryChildFragment.effectTag |= Placement;
primaryChildFragment.child = currentPrimaryChild;
currentPrimaryChild.return = primaryChildFragment;
// Create a fragment from the fallback children, too.
const fallbackChildFragment = (primaryChildFragment.sibling = createFiberFromFragment(
nextFallbackChildren,
mode,
renderExpirationTime,
null,
));
fallbackChildFragment.effectTag |= Placement;
child = primaryChildFragment;
// Skip the primary children, and continue working on the
// fallback children.
next = fallbackChildFragment;
child.return = next.return = workInProgress;
} else {
// Still haven't timed out. Continue rendering the children, like we
// normally do.
const nextPrimaryChildren = nextProps.children;
next = child = reconcileChildFibers(
workInProgress,
currentPrimaryChild,
nextPrimaryChildren,
renderExpirationTime,
);
}
}
}
workInProgress.memoizedState = nextState;
workInProgress.child = child;
return next;
}
function updatePortalComponent(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
) {
pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo);
const nextChildren = workInProgress.pendingProps;
if (current === null) {
// Portals are special because we don't append the children during mount
// but at commit. Therefore we need to track insertions which the normal
// flow doesn't do during mount. This doesn't happen at the root because
// the root always starts with a "current" with a null child.
// TODO: Consider unifying this with how the root works.
workInProgress.child = reconcileChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
} else {
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
}
return workInProgress.child;
}
function updateContextProvider(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
) {
const providerType: ReactProviderType<any> = workInProgress.type;
const context: ReactContext<any> = providerType._context;
const newProps = workInProgress.pendingProps;
const oldProps = workInProgress.memoizedProps;
const newValue = newProps.value;
if (__DEV__) {
const providerPropTypes = workInProgress.type.propTypes;
if (providerPropTypes) {
checkPropTypes(
providerPropTypes,
newProps,
'prop',
'Context.Provider',
ReactCurrentFiber.getCurrentFiberStackInDev,
);
}
}
pushProvider(workInProgress, newValue);
if (oldProps !== null) {
const oldValue = oldProps.value;
const changedBits = calculateChangedBits(context, newValue, oldValue);
if (changedBits === 0) {
// No change. Bailout early if children are the same.
if (
oldProps.children === newProps.children &&
!hasLegacyContextChanged()
) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
} else {
// The context value changed. Search for matching consumers and schedule
// them to update.
propagateContextChange(
workInProgress,
context,
changedBits,
renderExpirationTime,
);
}
}
const newChildren = newProps.children;
reconcileChildren(current, workInProgress, newChildren, renderExpirationTime);
return workInProgress.child;
}
let hasWarnedAboutUsingContextAsConsumer = false;
function updateContextConsumer(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
) {
let context: ReactContext<any> = workInProgress.type;
// The logic below for Context differs depending on PROD or DEV mode. In
// DEV mode, we create a separate object for Context.Consumer that acts
// like a proxy to Context. This proxy object adds unnecessary code in PROD
// so we use the old behaviour (Context.Consumer references Context) to
// reduce size and overhead. The separate object references context via
// a property called "_context", which also gives us the ability to check
// in DEV mode if this property exists or not and warn if it does not.
if (__DEV__) {
if ((context: any)._context === undefined) {
// This may be because it's a Context (rather than a Consumer).
// Or it may be because it's older React where they're the same thing.
// We only want to warn if we're sure it's a new React.
if (context !== context.Consumer) {
if (!hasWarnedAboutUsingContextAsConsumer) {
hasWarnedAboutUsingContextAsConsumer = true;
warning(
false,
'Rendering <Context> directly is not supported and will be removed in ' +
'a future major release. Did you mean to render <Context.Consumer> instead?',
);
}
}
} else {
context = (context: any)._context;
}
}
const newProps = workInProgress.pendingProps;
const render = newProps.children;
if (__DEV__) {
warningWithoutStack(
typeof render === 'function',
'A context consumer was rendered with multiple children, or a child ' +
"that isn't a function. A context consumer expects a single child " +
'that is a function. If you did pass a function, make sure there ' +
'is no trailing or leading whitespace around it.',
);
}
prepareToReadContext(workInProgress, renderExpirationTime);
const newValue = readContext(context, newProps.unstable_observedBits);
let newChildren;
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
ReactCurrentFiber.setCurrentPhase('render');
newChildren = render(newValue);
ReactCurrentFiber.setCurrentPhase(null);
} else {
newChildren = render(newValue);
}
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
reconcileChildren(current, workInProgress, newChildren, renderExpirationTime);
return workInProgress.child;
}
/*
function reuseChildrenEffects(returnFiber : Fiber, firstChild : Fiber) {
let child = firstChild;
do {
// Ensure that the first and last effect of the parent corresponds
// to the children's first and last effect.
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = child.firstEffect;
}
if (child.lastEffect) {
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = child.firstEffect;
}
returnFiber.lastEffect = child.lastEffect;
}
} while (child = child.sibling);
}
*/
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
cancelWorkTimer(workInProgress);
if (current !== null) {
// Reuse previous context list
workInProgress.firstContextDependency = current.firstContextDependency;
}
if (enableProfilerTimer) {
// Don't update "base" render times for bailouts.
stopProfilerTimerIfRunning(workInProgress);
}
// Check if the children have any pending work.
const childExpirationTime = workInProgress.childExpirationTime;
if (
childExpirationTime === NoWork ||
childExpirationTime > renderExpirationTime
) {
// The children don't have any work either. We can skip them.
// TODO: Once we add back resuming, we should check if the children are
// a work-in-progress set. If so, we need to transfer their effects.
return null;
} else {
// This fiber doesn't have work, but its subtree does. Clone the child
// fibers and continue.
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
}
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
const updateExpirationTime = workInProgress.expirationTime;
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps === newProps &&
!hasLegacyContextChanged() &&
(updateExpirationTime === NoWork ||
updateExpirationTime > renderExpirationTime)
) {
// This fiber does not have any pending work. Bailout without entering
// the begin phase. There's still some bookkeeping we that needs to be done
// in this optimized path, mostly pushing stuff onto the stack.
switch (workInProgress.tag) {
case HostRoot:
pushHostRootContext(workInProgress);
resetHydrationState();
break;
case HostComponent:
pushHostContext(workInProgress);
break;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
pushLegacyContextProvider(workInProgress);
}
break;
}
case ClassComponentLazy: {
const thenable = workInProgress.type;
const Component = getResultFromResolvedLazyComponent(thenable);
if (isLegacyContextProvider(Component)) {
pushLegacyContextProvider(workInProgress);
}
break;
}
case HostPortal:
pushHostContainer(
workInProgress,
workInProgress.stateNode.containerInfo,
);
break;
case ContextProvider: {
const newValue = workInProgress.memoizedProps.value;
pushProvider(workInProgress, newValue);
break;
}
case Profiler:
if (enableProfilerTimer) {
workInProgress.effectTag |= Update;
}
break;
case SuspenseComponent: {
const child = bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
if (child !== null) {
const nextState = workInProgress.memoizedState;
const nextDidTimeout = nextState !== null && nextState.didTimeout;
if (nextDidTimeout) {
return child.sibling;
} else {
return child;
}
} else {
return null;
}
}
}
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
}
// Before entering the begin phase, clear the expiration time.
workInProgress.expirationTime = NoWork;
switch (workInProgress.tag) {
case IndeterminateComponent: {
const Component = workInProgress.type;
return mountIndeterminateComponent(
current,
workInProgress,
Component,
updateExpirationTime,
renderExpirationTime,
);
}
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
return updateFunctionComponent(
current,
workInProgress,
Component,
unresolvedProps,
renderExpirationTime,
);
}
case FunctionComponentLazy: {
const thenable = workInProgress.type;
const Component = getResultFromResolvedLazyComponent(thenable);
const unresolvedProps = workInProgress.pendingProps;
const child = updateFunctionComponent(
current,
workInProgress,
Component,
resolveDefaultProps(Component, unresolvedProps),
renderExpirationTime,
);
return child;
}
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
return updateClassComponent(
current,
workInProgress,
Component,
unresolvedProps,
renderExpirationTime,
);
}
case ClassComponentLazy: {
const thenable = workInProgress.type;
const Component = getResultFromResolvedLazyComponent(thenable);
const unresolvedProps = workInProgress.pendingProps;
const child = updateClassComponent(
current,
workInProgress,
Component,
resolveDefaultProps(Component, unresolvedProps),
renderExpirationTime,
);
return child;
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderExpirationTime);
case HostComponent:
return updateHostComponent(current, workInProgress, renderExpirationTime);
case HostText:
return updateHostText(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(
current,
workInProgress,
renderExpirationTime,
);
case HostPortal:
return updatePortalComponent(
current,
workInProgress,
renderExpirationTime,
);
case ForwardRef: {
const type = workInProgress.type;
return updateForwardRef(
current,
workInProgress,
type,
workInProgress.pendingProps,
renderExpirationTime,
);
}
case ForwardRefLazy: {
const thenable = workInProgress.type;
const Component = getResultFromResolvedLazyComponent(thenable);
const unresolvedProps = workInProgress.pendingProps;
const child = updateForwardRef(
current,
workInProgress,
Component,
resolveDefaultProps(Component, unresolvedProps),
renderExpirationTime,
);
return child;
}
case Fragment:
return updateFragment(current, workInProgress, renderExpirationTime);
case Mode:
return updateMode(current, workInProgress, renderExpirationTime);
case Profiler:
return updateProfiler(current, workInProgress, renderExpirationTime);
case ContextProvider:
return updateContextProvider(
current,
workInProgress,
renderExpirationTime,
);
case ContextConsumer:
return updateContextConsumer(
current,
workInProgress,
renderExpirationTime,
);
case PureComponent: {
const type = workInProgress.type;
return updatePureComponent(
current,
workInProgress,
type,
workInProgress.pendingProps,
updateExpirationTime,
renderExpirationTime,
);
}
case PureComponentLazy: {
const thenable = workInProgress.type;
const Component = getResultFromResolvedLazyComponent(thenable);
const unresolvedProps = workInProgress.pendingProps;
const child = updatePureComponent(
current,
workInProgress,
Component,
resolveDefaultProps(Component, unresolvedProps),
updateExpirationTime,
renderExpirationTime,
);
return child;
}
default:
invariant(
false,
'Unknown unit of work tag. This error is likely caused by a bug in ' +
'React. Please file an issue.',
);
}
}
export {beginWork};