Files
react/packages/react-reconciler/src/ReactFiberCompleteWork.js
T

812 lines
26 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 {Fiber} from './ReactFiber';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {FiberRoot} from './ReactFiberRoot';
import type {
Instance,
Type,
Props,
Container,
ChildSet,
} from './ReactFiberHostConfig';
import {
IndeterminateComponent,
FunctionComponent,
ClassComponent,
HostRoot,
HostComponent,
HostText,
HostPortal,
ContextProvider,
ContextConsumer,
ForwardRef,
Fragment,
Mode,
Profiler,
SuspenseComponent,
DehydratedSuspenseComponent,
MemoComponent,
SimpleMemoComponent,
LazyComponent,
IncompleteClassComponent,
EventComponent,
EventTarget,
} from 'shared/ReactWorkTags';
import {
Placement,
Ref,
Update,
NoEffect,
DidCapture,
Deletion,
} from 'shared/ReactSideEffectTags';
import invariant from 'shared/invariant';
import {
createInstance,
createTextInstance,
appendInitialChild,
finalizeInitialChildren,
prepareUpdate,
supportsMutation,
supportsPersistence,
cloneInstance,
cloneHiddenInstance,
cloneHiddenTextInstance,
createContainerChildSet,
appendChildToContainerChildSet,
finalizeContainerChildren,
handleEventComponent,
handleEventTarget,
} from './ReactFiberHostConfig';
import {
getRootHostContainer,
popHostContext,
getHostContext,
popHostContainer,
} from './ReactFiberHostContext';
import {
isContextProvider as isLegacyContextProvider,
popContext as popLegacyContext,
popTopLevelContextObject as popTopLevelLegacyContextObject,
} from './ReactFiberContext';
import {popProvider} from './ReactFiberNewContext';
import {
prepareToHydrateHostInstance,
prepareToHydrateHostTextInstance,
skipPastDehydratedSuspenseInstance,
popHydrationState,
} from './ReactFiberHydrationContext';
import {
enableSuspenseServerRenderer,
enableEventAPI,
} from 'shared/ReactFeatureFlags';
function markUpdate(workInProgress: Fiber) {
// Tag the fiber with an update effect. This turns a Placement into
// a PlacementAndUpdate.
workInProgress.effectTag |= Update;
}
function markRef(workInProgress: Fiber) {
workInProgress.effectTag |= Ref;
}
let appendAllChildren;
let updateHostContainer;
let updateHostComponent;
let updateHostText;
if (supportsMutation) {
// Mutation mode
appendAllChildren = function(
parent: Instance,
workInProgress: Fiber,
needsVisibilityToggle: boolean,
isHidden: boolean,
) {
// We only have the top Fiber that was created but we need recurse down its
// children to find all the terminal nodes.
let node = workInProgress.child;
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
appendInitialChild(parent, node.stateNode);
} else if (node.tag === HostPortal) {
// If we have a portal child, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
if (node === workInProgress) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
};
updateHostContainer = function(workInProgress: Fiber) {
// Noop
};
updateHostComponent = function(
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
rootContainerInstance: Container,
) {
// If we have an alternate, that means this is an update and we need to
// schedule a side-effect to do the updates.
const oldProps = current.memoizedProps;
if (oldProps === newProps) {
// In mutation mode, this is sufficient for a bailout because
// we won't touch this node even if children changed.
return;
}
// If we get updated because one of our children updated, we don't
// have newProps so we'll have to reuse them.
// TODO: Split the update API as separate for the props vs. children.
// Even better would be if children weren't special cased at all tho.
const instance: Instance = workInProgress.stateNode;
const currentHostContext = getHostContext();
// TODO: Experiencing an error where oldProps is null. Suggests a host
// component is hitting the resume path. Figure out why. Possibly
// related to `hidden`.
const updatePayload = prepareUpdate(
instance,
type,
oldProps,
newProps,
rootContainerInstance,
currentHostContext,
);
// TODO: Type this specific to this type of component.
workInProgress.updateQueue = (updatePayload: any);
// If the update payload indicates that there is a change or if there
// is a new ref we mark this as an update. All the work is done in commitWork.
if (updatePayload) {
markUpdate(workInProgress);
}
};
updateHostText = function(
current: Fiber,
workInProgress: Fiber,
oldText: string,
newText: string,
) {
// If the text differs, mark it as an update. All the work in done in commitWork.
if (oldText !== newText) {
markUpdate(workInProgress);
}
};
} else if (supportsPersistence) {
// Persistent host tree mode
appendAllChildren = function(
parent: Instance,
workInProgress: Fiber,
needsVisibilityToggle: boolean,
isHidden: boolean,
) {
// We only have the top Fiber that was created but we need recurse down its
// children to find all the terminal nodes.
let node = workInProgress.child;
while (node !== null) {
// eslint-disable-next-line no-labels
branches: if (node.tag === HostComponent) {
let instance = node.stateNode;
if (needsVisibilityToggle && isHidden) {
// This child is inside a timed out tree. Hide it.
const props = node.memoizedProps;
const type = node.type;
instance = cloneHiddenInstance(instance, type, props, node);
}
appendInitialChild(parent, instance);
} else if (node.tag === HostText) {
let instance = node.stateNode;
if (needsVisibilityToggle && isHidden) {
// This child is inside a timed out tree. Hide it.
const text = node.memoizedProps;
instance = cloneHiddenTextInstance(instance, text, node);
}
appendInitialChild(parent, instance);
} else if (node.tag === HostPortal) {
// If we have a portal child, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else if (node.tag === SuspenseComponent) {
if ((node.effectTag & Update) !== NoEffect) {
// Need to toggle the visibility of the primary children.
const newIsHidden = node.memoizedState !== null;
if (newIsHidden) {
const primaryChildParent = node.child;
if (primaryChildParent !== null) {
if (primaryChildParent.child !== null) {
primaryChildParent.child.return = primaryChildParent;
appendAllChildren(
parent,
primaryChildParent,
true,
newIsHidden,
);
}
const fallbackChildParent = primaryChildParent.sibling;
if (fallbackChildParent !== null) {
fallbackChildParent.return = node;
node = fallbackChildParent;
continue;
}
}
}
}
if (node.child !== null) {
// Continue traversing like normal
node.child.return = node;
node = node.child;
continue;
}
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
// $FlowFixMe This is correct but Flow is confused by the labeled break.
node = (node: Fiber);
if (node === workInProgress) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
};
// An unfortunate fork of appendAllChildren because we have two different parent types.
const appendAllChildrenToContainer = function(
containerChildSet: ChildSet,
workInProgress: Fiber,
needsVisibilityToggle: boolean,
isHidden: boolean,
) {
// We only have the top Fiber that was created but we need recurse down its
// children to find all the terminal nodes.
let node = workInProgress.child;
while (node !== null) {
// eslint-disable-next-line no-labels
branches: if (node.tag === HostComponent) {
let instance = node.stateNode;
if (needsVisibilityToggle && isHidden) {
// This child is inside a timed out tree. Hide it.
const props = node.memoizedProps;
const type = node.type;
instance = cloneHiddenInstance(instance, type, props, node);
}
appendChildToContainerChildSet(containerChildSet, instance);
} else if (node.tag === HostText) {
let instance = node.stateNode;
if (needsVisibilityToggle && isHidden) {
// This child is inside a timed out tree. Hide it.
const text = node.memoizedProps;
instance = cloneHiddenTextInstance(instance, text, node);
}
appendChildToContainerChildSet(containerChildSet, instance);
} else if (node.tag === HostPortal) {
// If we have a portal child, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else if (node.tag === SuspenseComponent) {
if ((node.effectTag & Update) !== NoEffect) {
// Need to toggle the visibility of the primary children.
const newIsHidden = node.memoizedState !== null;
if (newIsHidden) {
const primaryChildParent = node.child;
if (primaryChildParent !== null) {
if (primaryChildParent.child !== null) {
primaryChildParent.child.return = primaryChildParent;
appendAllChildrenToContainer(
containerChildSet,
primaryChildParent,
true,
newIsHidden,
);
}
const fallbackChildParent = primaryChildParent.sibling;
if (fallbackChildParent !== null) {
fallbackChildParent.return = node;
node = fallbackChildParent;
continue;
}
}
}
}
if (node.child !== null) {
// Continue traversing like normal
node.child.return = node;
node = node.child;
continue;
}
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
// $FlowFixMe This is correct but Flow is confused by the labeled break.
node = (node: Fiber);
if (node === workInProgress) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
};
updateHostContainer = function(workInProgress: Fiber) {
const portalOrRoot: {
containerInfo: Container,
pendingChildren: ChildSet,
} =
workInProgress.stateNode;
const childrenUnchanged = workInProgress.firstEffect === null;
if (childrenUnchanged) {
// No changes, just reuse the existing instance.
} else {
const container = portalOrRoot.containerInfo;
let newChildSet = createContainerChildSet(container);
// If children might have changed, we have to add them all to the set.
appendAllChildrenToContainer(newChildSet, workInProgress, false, false);
portalOrRoot.pendingChildren = newChildSet;
// Schedule an update on the container to swap out the container.
markUpdate(workInProgress);
finalizeContainerChildren(container, newChildSet);
}
};
updateHostComponent = function(
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
rootContainerInstance: Container,
) {
const currentInstance = current.stateNode;
const oldProps = current.memoizedProps;
// If there are no effects associated with this node, then none of our children had any updates.
// This guarantees that we can reuse all of them.
const childrenUnchanged = workInProgress.firstEffect === null;
if (childrenUnchanged && oldProps === newProps) {
// No changes, just reuse the existing instance.
// Note that this might release a previous clone.
workInProgress.stateNode = currentInstance;
return;
}
const recyclableInstance: Instance = workInProgress.stateNode;
const currentHostContext = getHostContext();
let updatePayload = null;
if (oldProps !== newProps) {
updatePayload = prepareUpdate(
recyclableInstance,
type,
oldProps,
newProps,
rootContainerInstance,
currentHostContext,
);
}
if (childrenUnchanged && updatePayload === null) {
// No changes, just reuse the existing instance.
// Note that this might release a previous clone.
workInProgress.stateNode = currentInstance;
return;
}
let newInstance = cloneInstance(
currentInstance,
updatePayload,
type,
oldProps,
newProps,
workInProgress,
childrenUnchanged,
recyclableInstance,
);
if (
finalizeInitialChildren(
newInstance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
workInProgress.stateNode = newInstance;
if (childrenUnchanged) {
// If there are no other effects in this tree, we need to flag this node as having one.
// Even though we're not going to use it for anything.
// Otherwise parents won't know that there are new children to propagate upwards.
markUpdate(workInProgress);
} else {
// If children might have changed, we have to add them all to the set.
appendAllChildren(newInstance, workInProgress, false, false);
}
};
updateHostText = function(
current: Fiber,
workInProgress: Fiber,
oldText: string,
newText: string,
) {
if (oldText !== newText) {
// If the text content differs, we'll create a new text instance for it.
const rootContainerInstance = getRootHostContainer();
const currentHostContext = getHostContext();
workInProgress.stateNode = createTextInstance(
newText,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// We'll have to mark it as having an effect, even though we won't use the effect for anything.
// This lets the parents know that at least one of their children has changed.
markUpdate(workInProgress);
}
};
} else {
// No host operations
updateHostContainer = function(workInProgress: Fiber) {
// Noop
};
updateHostComponent = function(
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
rootContainerInstance: Container,
) {
// Noop
};
updateHostText = function(
current: Fiber,
workInProgress: Fiber,
oldText: string,
newText: string,
) {
// Noop
};
}
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
break;
case LazyComponent:
break;
case SimpleMemoComponent:
case FunctionComponent:
break;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
break;
}
case HostRoot: {
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
const fiberRoot = (workInProgress.stateNode: FiberRoot);
if (fiberRoot.pendingContext) {
fiberRoot.context = fiberRoot.pendingContext;
fiberRoot.pendingContext = null;
}
if (current === null || current.child === null) {
// If we hydrated, pop so that we can delete any remaining children
// that weren't hydrated.
popHydrationState(workInProgress);
// This resets the hacky state to fix isMounted before committing.
// TODO: Delete this when we delete isMounted and findDOMNode.
workInProgress.effectTag &= ~Placement;
}
updateHostContainer(workInProgress);
break;
}
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
if (!newProps) {
invariant(
workInProgress.stateNode !== null,
'We must have new props for new mounts. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
// This can happen when we abort work.
break;
}
const currentHostContext = getHostContext();
// TODO: Move createInstance to beginWork and keep it on a context
// "stack" as the parent. Then append children as we go in beginWork
// or completeWork depending on we want to add then top->down or
// bottom->up. Top->down is faster in IE11.
let wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// TODO: Move this and createInstance step into the beginPhase
// to consolidate.
if (
prepareToHydrateHostInstance(
workInProgress,
rootContainerInstance,
currentHostContext,
)
) {
// If changes to the hydrated node needs to be applied at the
// commit-phase we mark this as such.
markUpdate(workInProgress);
}
} else {
let instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
appendAllChildren(instance, workInProgress, false, false);
// Certain renderers require commit-time effects for initial mount.
// (eg DOM renderer supports auto-focus for certain elements).
// Make sure such renderers get scheduled for later work.
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
workInProgress.stateNode = instance;
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef(workInProgress);
}
}
break;
}
case HostText: {
let newText = newProps;
if (current && workInProgress.stateNode != null) {
const oldText = current.memoizedProps;
// If we have an alternate, that means this is an update and we need
// to schedule a side-effect to do the updates.
updateHostText(current, workInProgress, oldText, newText);
} else {
if (typeof newText !== 'string') {
invariant(
workInProgress.stateNode !== null,
'We must have new props for new mounts. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
// This can happen when we abort work.
}
const rootContainerInstance = getRootHostContainer();
const currentHostContext = getHostContext();
let wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
if (prepareToHydrateHostTextInstance(workInProgress)) {
markUpdate(workInProgress);
}
} else {
workInProgress.stateNode = createTextInstance(
newText,
rootContainerInstance,
currentHostContext,
workInProgress,
);
}
}
break;
}
case ForwardRef:
break;
case SuspenseComponent: {
const nextState = workInProgress.memoizedState;
if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
// Something suspended. Re-render with the fallback children.
workInProgress.expirationTime = renderExpirationTime;
// Do not reset the effect list.
return workInProgress;
}
const nextDidTimeout = nextState !== null;
const prevDidTimeout = current !== null && current.memoizedState !== null;
if (current === null) {
// In cases where we didn't find a suitable hydration boundary we never
// downgraded this to a DehydratedSuspenseComponent, but we still need to
// pop the hydration state since we might be inside the insertion tree.
popHydrationState(workInProgress);
} else if (!nextDidTimeout && prevDidTimeout) {
// We just switched from the fallback to the normal children. Delete
// the fallback.
// TODO: Would it be better to store the fallback fragment on
// the stateNode during the begin phase?
const currentFallbackChild: Fiber | null = (current.child: any).sibling;
if (currentFallbackChild !== null) {
// Deletions go at the beginning of the return fiber's effect list
const first = workInProgress.firstEffect;
if (first !== null) {
workInProgress.firstEffect = currentFallbackChild;
currentFallbackChild.nextEffect = first;
} else {
workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChild;
currentFallbackChild.nextEffect = null;
}
currentFallbackChild.effectTag = Deletion;
}
}
if (supportsPersistence) {
if (nextDidTimeout) {
// If this boundary just timed out, schedule an effect to attach a
// retry listener to the proimse. This flag is also used to hide the
// primary children.
workInProgress.effectTag |= Update;
}
}
if (supportsMutation) {
if (nextDidTimeout || prevDidTimeout) {
// If this boundary just timed out, schedule an effect to attach a
// retry listener to the proimse. This flag is also used to hide the
// primary children. In mutation mode, we also need the flag to
// *unhide* children that were previously hidden, so check if the
// is currently timed out, too.
workInProgress.effectTag |= Update;
}
}
break;
}
case Fragment:
break;
case Mode:
break;
case Profiler:
break;
case HostPortal:
popHostContainer(workInProgress);
updateHostContainer(workInProgress);
break;
case ContextProvider:
// Pop provider fiber
popProvider(workInProgress);
break;
case ContextConsumer:
break;
case MemoComponent:
break;
case IncompleteClassComponent: {
// Same as class component case. I put it down here so that the tags are
// sequential to ensure this switch is compiled to a jump table.
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
break;
}
case DehydratedSuspenseComponent: {
if (enableSuspenseServerRenderer) {
if (current === null) {
let wasHydrated = popHydrationState(workInProgress);
invariant(
wasHydrated,
'A dehydrated suspense component was completed without a hydrated node. ' +
'This is probably a bug in React.',
);
skipPastDehydratedSuspenseInstance(workInProgress);
} else if ((workInProgress.effectTag & DidCapture) === NoEffect) {
// This boundary did not suspend so it's now hydrated.
// To handle any future suspense cases, we're going to now upgrade it
// to a Suspense component. We detach it from the existing current fiber.
current.alternate = null;
workInProgress.alternate = null;
workInProgress.tag = SuspenseComponent;
workInProgress.memoizedState = null;
workInProgress.stateNode = null;
}
}
break;
}
case EventComponent: {
if (enableEventAPI) {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const responder = workInProgress.type.responder;
// Update the props on the event component state node
workInProgress.stateNode.props = newProps;
handleEventComponent(responder, rootContainerInstance, workInProgress);
}
break;
}
case EventTarget: {
if (enableEventAPI) {
popHostContext(workInProgress);
const type = workInProgress.type.type;
const rootContainerInstance = getRootHostContainer();
const shouldUpdate = handleEventTarget(
type,
newProps,
rootContainerInstance,
workInProgress,
);
if (shouldUpdate) {
markUpdate(workInProgress);
}
}
break;
}
default:
invariant(
false,
'Unknown unit of work tag. This error is likely caused by a bug in ' +
'React. Please file an issue.',
);
}
return null;
}
export {completeWork};