mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
I had thought I wouldn't fork this one because the new "lanes" module will be pretty different, but I need it to make some incremental changes.
1315 lines
47 KiB
JavaScript
1315 lines
47 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 './ReactInternalTypes';
|
|
import type {ExpirationTime} from './ReactFiberExpirationTime.new';
|
|
import type {
|
|
ReactFundamentalComponentInstance,
|
|
ReactScopeInstance,
|
|
} from 'shared/ReactTypes';
|
|
import type {FiberRoot} from './ReactInternalTypes';
|
|
import type {
|
|
Instance,
|
|
Type,
|
|
Props,
|
|
Container,
|
|
ChildSet,
|
|
} from './ReactFiberHostConfig';
|
|
import type {
|
|
SuspenseState,
|
|
SuspenseListRenderState,
|
|
} from './ReactFiberSuspenseComponent.new';
|
|
import type {SuspenseContext} from './ReactFiberSuspenseContext.new';
|
|
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new';
|
|
|
|
import {now} from './SchedulerWithReactIntegration.new';
|
|
|
|
import {
|
|
IndeterminateComponent,
|
|
FunctionComponent,
|
|
ClassComponent,
|
|
HostRoot,
|
|
HostComponent,
|
|
HostText,
|
|
HostPortal,
|
|
ContextProvider,
|
|
ContextConsumer,
|
|
ForwardRef,
|
|
Fragment,
|
|
Mode,
|
|
Profiler,
|
|
SuspenseComponent,
|
|
SuspenseListComponent,
|
|
MemoComponent,
|
|
SimpleMemoComponent,
|
|
LazyComponent,
|
|
IncompleteClassComponent,
|
|
FundamentalComponent,
|
|
ScopeComponent,
|
|
Block,
|
|
} from './ReactWorkTags';
|
|
import {NoMode, BlockingMode} from './ReactTypeOfMode';
|
|
import {
|
|
Ref,
|
|
Update,
|
|
NoEffect,
|
|
DidCapture,
|
|
Deletion,
|
|
} from './ReactSideEffectTags';
|
|
import invariant from 'shared/invariant';
|
|
|
|
import {
|
|
createInstance,
|
|
createTextInstance,
|
|
appendInitialChild,
|
|
finalizeInitialChildren,
|
|
prepareUpdate,
|
|
supportsMutation,
|
|
supportsPersistence,
|
|
cloneInstance,
|
|
cloneHiddenInstance,
|
|
cloneHiddenTextInstance,
|
|
createContainerChildSet,
|
|
appendChildToContainerChildSet,
|
|
finalizeContainerChildren,
|
|
getFundamentalComponentInstance,
|
|
mountFundamentalComponent,
|
|
cloneFundamentalInstance,
|
|
shouldUpdateFundamentalComponent,
|
|
} from './ReactFiberHostConfig';
|
|
import {
|
|
getRootHostContainer,
|
|
popHostContext,
|
|
getHostContext,
|
|
popHostContainer,
|
|
} from './ReactFiberHostContext.new';
|
|
import {
|
|
suspenseStackCursor,
|
|
InvisibleParentSuspenseContext,
|
|
hasSuspenseContext,
|
|
popSuspenseContext,
|
|
pushSuspenseContext,
|
|
setShallowSuspenseContext,
|
|
ForceSuspenseFallback,
|
|
setDefaultShallowSuspenseContext,
|
|
} from './ReactFiberSuspenseContext.new';
|
|
import {findFirstSuspended} from './ReactFiberSuspenseComponent.new';
|
|
import {
|
|
isContextProvider as isLegacyContextProvider,
|
|
popContext as popLegacyContext,
|
|
popTopLevelContextObject as popTopLevelLegacyContextObject,
|
|
} from './ReactFiberContext.new';
|
|
import {popProvider} from './ReactFiberNewContext.new';
|
|
import {
|
|
prepareToHydrateHostInstance,
|
|
prepareToHydrateHostTextInstance,
|
|
prepareToHydrateHostSuspenseInstance,
|
|
popHydrationState,
|
|
resetHydrationState,
|
|
} from './ReactFiberHydrationContext.new';
|
|
import {
|
|
enableSchedulerTracing,
|
|
enableSuspenseCallback,
|
|
enableSuspenseServerRenderer,
|
|
enableDeprecatedFlareAPI,
|
|
enableFundamentalAPI,
|
|
enableScopeAPI,
|
|
enableBlocksAPI,
|
|
} from 'shared/ReactFeatureFlags';
|
|
import {
|
|
markSpawnedWork,
|
|
renderDidSuspend,
|
|
renderDidSuspendDelayIfPossible,
|
|
renderHasNotSuspendedYet,
|
|
} from './ReactFiberWorkLoop.new';
|
|
import {createFundamentalStateInstance} from './ReactFiberFundamental.new';
|
|
import {Never} from './ReactFiberExpirationTime.new';
|
|
import {resetChildFibers} from './ReactChildFiber.new';
|
|
import {updateDeprecatedEventListeners} from './ReactFiberDeprecatedEvents.new';
|
|
import {createScopeMethods} from './ReactFiberScope.new';
|
|
|
|
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 (enableFundamentalAPI && node.tag === FundamentalComponent) {
|
|
appendInitialChild(parent, node.stateNode.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.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 (enableFundamentalAPI && node.tag === FundamentalComponent) {
|
|
let instance = node.stateNode.instance;
|
|
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 === 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 (enableFundamentalAPI && node.tag === FundamentalComponent) {
|
|
let instance = node.stateNode.instance;
|
|
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 === 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;
|
|
const 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;
|
|
}
|
|
const 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 {
|
|
workInProgress.stateNode = current.stateNode;
|
|
}
|
|
};
|
|
} 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 cutOffTailIfNeeded(
|
|
renderState: SuspenseListRenderState,
|
|
hasRenderedATailFallback: boolean,
|
|
) {
|
|
switch (renderState.tailMode) {
|
|
case 'hidden': {
|
|
// Any insertions at the end of the tail list after this point
|
|
// should be invisible. If there are already mounted boundaries
|
|
// anything before them are not considered for collapsing.
|
|
// Therefore we need to go through the whole tail to find if
|
|
// there are any.
|
|
let tailNode = renderState.tail;
|
|
let lastTailNode = null;
|
|
while (tailNode !== null) {
|
|
if (tailNode.alternate !== null) {
|
|
lastTailNode = tailNode;
|
|
}
|
|
tailNode = tailNode.sibling;
|
|
}
|
|
// Next we're simply going to delete all insertions after the
|
|
// last rendered item.
|
|
if (lastTailNode === null) {
|
|
// All remaining items in the tail are insertions.
|
|
renderState.tail = null;
|
|
} else {
|
|
// Detach the insertion after the last node that was already
|
|
// inserted.
|
|
lastTailNode.sibling = null;
|
|
}
|
|
break;
|
|
}
|
|
case 'collapsed': {
|
|
// Any insertions at the end of the tail list after this point
|
|
// should be invisible. If there are already mounted boundaries
|
|
// anything before them are not considered for collapsing.
|
|
// Therefore we need to go through the whole tail to find if
|
|
// there are any.
|
|
let tailNode = renderState.tail;
|
|
let lastTailNode = null;
|
|
while (tailNode !== null) {
|
|
if (tailNode.alternate !== null) {
|
|
lastTailNode = tailNode;
|
|
}
|
|
tailNode = tailNode.sibling;
|
|
}
|
|
// Next we're simply going to delete all insertions after the
|
|
// last rendered item.
|
|
if (lastTailNode === null) {
|
|
// All remaining items in the tail are insertions.
|
|
if (!hasRenderedATailFallback && renderState.tail !== null) {
|
|
// We suspended during the head. We want to show at least one
|
|
// row at the tail. So we'll keep on and cut off the rest.
|
|
renderState.tail.sibling = null;
|
|
} else {
|
|
renderState.tail = null;
|
|
}
|
|
} else {
|
|
// Detach the insertion after the last node that was already
|
|
// inserted.
|
|
lastTailNode.sibling = null;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function completeWork(
|
|
current: Fiber | null,
|
|
workInProgress: Fiber,
|
|
renderExpirationTime: ExpirationTime,
|
|
): Fiber | null {
|
|
const newProps = workInProgress.pendingProps;
|
|
|
|
switch (workInProgress.tag) {
|
|
case IndeterminateComponent:
|
|
case LazyComponent:
|
|
case SimpleMemoComponent:
|
|
case FunctionComponent:
|
|
case ForwardRef:
|
|
case Fragment:
|
|
case Mode:
|
|
case Profiler:
|
|
case ContextConsumer:
|
|
case MemoComponent:
|
|
return null;
|
|
case ClassComponent: {
|
|
const Component = workInProgress.type;
|
|
if (isLegacyContextProvider(Component)) {
|
|
popLegacyContext(workInProgress);
|
|
}
|
|
return null;
|
|
}
|
|
case HostRoot: {
|
|
popHostContainer(workInProgress);
|
|
popTopLevelLegacyContextObject(workInProgress);
|
|
resetMutableSourceWorkInProgressVersions();
|
|
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.
|
|
const wasHydrated = popHydrationState(workInProgress);
|
|
if (wasHydrated) {
|
|
// If we hydrated, then we'll need to schedule an update for
|
|
// the commit side-effects on the root.
|
|
markUpdate(workInProgress);
|
|
}
|
|
}
|
|
updateHostContainer(workInProgress);
|
|
return null;
|
|
}
|
|
case HostComponent: {
|
|
popHostContext(workInProgress);
|
|
const rootContainerInstance = getRootHostContainer();
|
|
const type = workInProgress.type;
|
|
if (current !== null && workInProgress.stateNode != null) {
|
|
updateHostComponent(
|
|
current,
|
|
workInProgress,
|
|
type,
|
|
newProps,
|
|
rootContainerInstance,
|
|
);
|
|
|
|
if (enableDeprecatedFlareAPI) {
|
|
const prevListeners = current.memoizedProps.DEPRECATED_flareListeners;
|
|
const nextListeners = newProps.DEPRECATED_flareListeners;
|
|
if (prevListeners !== nextListeners) {
|
|
markUpdate(workInProgress);
|
|
}
|
|
}
|
|
|
|
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.
|
|
return null;
|
|
}
|
|
|
|
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 whether we want to add them top->down or
|
|
// bottom->up. Top->down is faster in IE11.
|
|
const 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 need to be applied at the
|
|
// commit-phase we mark this as such.
|
|
markUpdate(workInProgress);
|
|
}
|
|
if (enableDeprecatedFlareAPI) {
|
|
const listeners = newProps.DEPRECATED_flareListeners;
|
|
if (listeners != null) {
|
|
updateDeprecatedEventListeners(
|
|
listeners,
|
|
workInProgress,
|
|
rootContainerInstance,
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
const instance = createInstance(
|
|
type,
|
|
newProps,
|
|
rootContainerInstance,
|
|
currentHostContext,
|
|
workInProgress,
|
|
);
|
|
|
|
appendAllChildren(instance, workInProgress, false, false);
|
|
|
|
// This needs to be set before we mount Flare event listeners
|
|
workInProgress.stateNode = instance;
|
|
|
|
if (enableDeprecatedFlareAPI) {
|
|
const listeners = newProps.DEPRECATED_flareListeners;
|
|
if (listeners != null) {
|
|
updateDeprecatedEventListeners(
|
|
listeners,
|
|
workInProgress,
|
|
rootContainerInstance,
|
|
);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
if (workInProgress.ref !== null) {
|
|
// If there is a ref on a host node we need to schedule a callback
|
|
markRef(workInProgress);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
case HostText: {
|
|
const 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();
|
|
const wasHydrated = popHydrationState(workInProgress);
|
|
if (wasHydrated) {
|
|
if (prepareToHydrateHostTextInstance(workInProgress)) {
|
|
markUpdate(workInProgress);
|
|
}
|
|
} else {
|
|
workInProgress.stateNode = createTextInstance(
|
|
newText,
|
|
rootContainerInstance,
|
|
currentHostContext,
|
|
workInProgress,
|
|
);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
case SuspenseComponent: {
|
|
popSuspenseContext(workInProgress);
|
|
const nextState: null | SuspenseState = workInProgress.memoizedState;
|
|
|
|
if (enableSuspenseServerRenderer) {
|
|
if (nextState !== null && nextState.dehydrated !== null) {
|
|
if (current === null) {
|
|
const wasHydrated = popHydrationState(workInProgress);
|
|
invariant(
|
|
wasHydrated,
|
|
'A dehydrated suspense component was completed without a hydrated node. ' +
|
|
'This is probably a bug in React.',
|
|
);
|
|
prepareToHydrateHostSuspenseInstance(workInProgress);
|
|
if (enableSchedulerTracing) {
|
|
markSpawnedWork(Never);
|
|
}
|
|
return null;
|
|
} else {
|
|
// We should never have been in a hydration state if we didn't have a current.
|
|
// However, in some of those paths, we might have reentered a hydration state
|
|
// and then we might be inside a hydration state. In that case, we'll need to exit out of it.
|
|
resetHydrationState();
|
|
if ((workInProgress.effectTag & DidCapture) === NoEffect) {
|
|
// This boundary did not suspend so it's now hydrated and unsuspended.
|
|
workInProgress.memoizedState = null;
|
|
}
|
|
// If nothing suspended, we need to schedule an effect to mark this boundary
|
|
// as having hydrated so events know that they're free to be invoked.
|
|
// It's also a signal to replay events and the suspense callback.
|
|
// If something suspended, schedule an effect to attach retry listeners.
|
|
// So we might as well always mark this.
|
|
workInProgress.effectTag |= Update;
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
let prevDidTimeout = false;
|
|
if (current === null) {
|
|
if (workInProgress.memoizedProps.fallback !== undefined) {
|
|
popHydrationState(workInProgress);
|
|
}
|
|
} else {
|
|
const prevState: null | SuspenseState = current.memoizedState;
|
|
prevDidTimeout = prevState !== null;
|
|
if (!nextDidTimeout && prevState !== null) {
|
|
// 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 (nextDidTimeout && !prevDidTimeout) {
|
|
// If this subtreee is running in blocking mode we can suspend,
|
|
// otherwise we won't suspend.
|
|
// TODO: This will still suspend a synchronous tree if anything
|
|
// in the concurrent tree already suspended during this render.
|
|
// This is a known bug.
|
|
if ((workInProgress.mode & BlockingMode) !== NoMode) {
|
|
// TODO: Move this back to throwException because this is too late
|
|
// if this is a large tree which is common for initial loads. We
|
|
// don't know if we should restart a render or not until we get
|
|
// this marker, and this is too late.
|
|
// If this render already had a ping or lower pri updates,
|
|
// and this is the first time we know we're going to suspend we
|
|
// should be able to immediately restart from within throwException.
|
|
const hasInvisibleChildContext =
|
|
current === null &&
|
|
workInProgress.memoizedProps.unstable_avoidThisFallback !== true;
|
|
if (
|
|
hasInvisibleChildContext ||
|
|
hasSuspenseContext(
|
|
suspenseStackCursor.current,
|
|
(InvisibleParentSuspenseContext: SuspenseContext),
|
|
)
|
|
) {
|
|
// If this was in an invisible tree or a new render, then showing
|
|
// this boundary is ok.
|
|
renderDidSuspend();
|
|
} else {
|
|
// Otherwise, we're going to have to hide content so we should
|
|
// suspend for longer if possible.
|
|
renderDidSuspendDelayIfPossible();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (supportsPersistence) {
|
|
// TODO: Only schedule updates if not prevDidTimeout.
|
|
if (nextDidTimeout) {
|
|
// If this boundary just timed out, schedule an effect to attach a
|
|
// retry listener to the promise. This flag is also used to hide the
|
|
// primary children.
|
|
workInProgress.effectTag |= Update;
|
|
}
|
|
}
|
|
if (supportsMutation) {
|
|
// TODO: Only schedule updates if these values are non equal, i.e. it changed.
|
|
if (nextDidTimeout || prevDidTimeout) {
|
|
// If this boundary just timed out, schedule an effect to attach a
|
|
// retry listener to the promise. 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 this
|
|
// is currently timed out, too.
|
|
workInProgress.effectTag |= Update;
|
|
}
|
|
}
|
|
if (
|
|
enableSuspenseCallback &&
|
|
workInProgress.updateQueue !== null &&
|
|
workInProgress.memoizedProps.suspenseCallback != null
|
|
) {
|
|
// Always notify the callback
|
|
workInProgress.effectTag |= Update;
|
|
}
|
|
return null;
|
|
}
|
|
case HostPortal:
|
|
popHostContainer(workInProgress);
|
|
updateHostContainer(workInProgress);
|
|
return null;
|
|
case ContextProvider:
|
|
// Pop provider fiber
|
|
popProvider(workInProgress);
|
|
return null;
|
|
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);
|
|
}
|
|
return null;
|
|
}
|
|
case SuspenseListComponent: {
|
|
popSuspenseContext(workInProgress);
|
|
|
|
const renderState: null | SuspenseListRenderState =
|
|
workInProgress.memoizedState;
|
|
|
|
if (renderState === null) {
|
|
// We're running in the default, "independent" mode.
|
|
// We don't do anything in this mode.
|
|
return null;
|
|
}
|
|
|
|
let didSuspendAlready =
|
|
(workInProgress.effectTag & DidCapture) !== NoEffect;
|
|
|
|
const renderedTail = renderState.rendering;
|
|
if (renderedTail === null) {
|
|
// We just rendered the head.
|
|
if (!didSuspendAlready) {
|
|
// This is the first pass. We need to figure out if anything is still
|
|
// suspended in the rendered set.
|
|
|
|
// If new content unsuspended, but there's still some content that
|
|
// didn't. Then we need to do a second pass that forces everything
|
|
// to keep showing their fallbacks.
|
|
|
|
// We might be suspended if something in this render pass suspended, or
|
|
// something in the previous committed pass suspended. Otherwise,
|
|
// there's no chance so we can skip the expensive call to
|
|
// findFirstSuspended.
|
|
const cannotBeSuspended =
|
|
renderHasNotSuspendedYet() &&
|
|
(current === null || (current.effectTag & DidCapture) === NoEffect);
|
|
if (!cannotBeSuspended) {
|
|
let row = workInProgress.child;
|
|
while (row !== null) {
|
|
const suspended = findFirstSuspended(row);
|
|
if (suspended !== null) {
|
|
didSuspendAlready = true;
|
|
workInProgress.effectTag |= DidCapture;
|
|
cutOffTailIfNeeded(renderState, false);
|
|
|
|
// If this is a newly suspended tree, it might not get committed as
|
|
// part of the second pass. In that case nothing will subscribe to
|
|
// its thennables. Instead, we'll transfer its thennables to the
|
|
// SuspenseList so that it can retry if they resolve.
|
|
// There might be multiple of these in the list but since we're
|
|
// going to wait for all of them anyway, it doesn't really matter
|
|
// which ones gets to ping. In theory we could get clever and keep
|
|
// track of how many dependencies remain but it gets tricky because
|
|
// in the meantime, we can add/remove/change items and dependencies.
|
|
// We might bail out of the loop before finding any but that
|
|
// doesn't matter since that means that the other boundaries that
|
|
// we did find already has their listeners attached.
|
|
const newThennables = suspended.updateQueue;
|
|
if (newThennables !== null) {
|
|
workInProgress.updateQueue = newThennables;
|
|
workInProgress.effectTag |= Update;
|
|
}
|
|
|
|
// Rerender the whole list, but this time, we'll force fallbacks
|
|
// to stay in place.
|
|
// Reset the effect list before doing the second pass since that's now invalid.
|
|
if (renderState.lastEffect === null) {
|
|
workInProgress.firstEffect = null;
|
|
}
|
|
workInProgress.lastEffect = renderState.lastEffect;
|
|
// Reset the child fibers to their original state.
|
|
resetChildFibers(workInProgress, renderExpirationTime);
|
|
|
|
// Set up the Suspense Context to force suspense and immediately
|
|
// rerender the children.
|
|
pushSuspenseContext(
|
|
workInProgress,
|
|
setShallowSuspenseContext(
|
|
suspenseStackCursor.current,
|
|
ForceSuspenseFallback,
|
|
),
|
|
);
|
|
return workInProgress.child;
|
|
}
|
|
row = row.sibling;
|
|
}
|
|
}
|
|
} else {
|
|
cutOffTailIfNeeded(renderState, false);
|
|
}
|
|
// Next we're going to render the tail.
|
|
} else {
|
|
// Append the rendered row to the child list.
|
|
if (!didSuspendAlready) {
|
|
const suspended = findFirstSuspended(renderedTail);
|
|
if (suspended !== null) {
|
|
workInProgress.effectTag |= DidCapture;
|
|
didSuspendAlready = true;
|
|
|
|
// Ensure we transfer the update queue to the parent so that it doesn't
|
|
// get lost if this row ends up dropped during a second pass.
|
|
const newThennables = suspended.updateQueue;
|
|
if (newThennables !== null) {
|
|
workInProgress.updateQueue = newThennables;
|
|
workInProgress.effectTag |= Update;
|
|
}
|
|
|
|
cutOffTailIfNeeded(renderState, true);
|
|
// This might have been modified.
|
|
if (
|
|
renderState.tail === null &&
|
|
renderState.tailMode === 'hidden' &&
|
|
!renderedTail.alternate
|
|
) {
|
|
// We need to delete the row we just rendered.
|
|
// Reset the effect list to what it was before we rendered this
|
|
// child. The nested children have already appended themselves.
|
|
const lastEffect = (workInProgress.lastEffect =
|
|
renderState.lastEffect);
|
|
// Remove any effects that were appended after this point.
|
|
if (lastEffect !== null) {
|
|
lastEffect.nextEffect = null;
|
|
}
|
|
// We're done.
|
|
return null;
|
|
}
|
|
} else if (
|
|
// The time it took to render last row is greater than time until
|
|
// the expiration.
|
|
now() * 2 - renderState.renderingStartTime >
|
|
renderState.tailExpiration &&
|
|
renderExpirationTime > Never
|
|
) {
|
|
// We have now passed our CPU deadline and we'll just give up further
|
|
// attempts to render the main content and only render fallbacks.
|
|
// The assumption is that this is usually faster.
|
|
workInProgress.effectTag |= DidCapture;
|
|
didSuspendAlready = true;
|
|
|
|
cutOffTailIfNeeded(renderState, false);
|
|
|
|
// Since nothing actually suspended, there will nothing to ping this
|
|
// to get it started back up to attempt the next item. If we can show
|
|
// them, then they really have the same priority as this render.
|
|
// So we'll pick it back up the very next render pass once we've had
|
|
// an opportunity to yield for paint.
|
|
|
|
const nextPriority = renderExpirationTime - 1;
|
|
workInProgress.expirationTime = workInProgress.childExpirationTime = nextPriority;
|
|
if (enableSchedulerTracing) {
|
|
markSpawnedWork(nextPriority);
|
|
}
|
|
}
|
|
}
|
|
if (renderState.isBackwards) {
|
|
// The effect list of the backwards tail will have been added
|
|
// to the end. This breaks the guarantee that life-cycles fire in
|
|
// sibling order but that isn't a strong guarantee promised by React.
|
|
// Especially since these might also just pop in during future commits.
|
|
// Append to the beginning of the list.
|
|
renderedTail.sibling = workInProgress.child;
|
|
workInProgress.child = renderedTail;
|
|
} else {
|
|
const previousSibling = renderState.last;
|
|
if (previousSibling !== null) {
|
|
previousSibling.sibling = renderedTail;
|
|
} else {
|
|
workInProgress.child = renderedTail;
|
|
}
|
|
renderState.last = renderedTail;
|
|
}
|
|
}
|
|
|
|
if (renderState.tail !== null) {
|
|
// We still have tail rows to render.
|
|
if (renderState.tailExpiration === 0) {
|
|
// Heuristic for how long we're willing to spend rendering rows
|
|
// until we just give up and show what we have so far.
|
|
const TAIL_EXPIRATION_TIMEOUT_MS = 500;
|
|
renderState.tailExpiration = now() + TAIL_EXPIRATION_TIMEOUT_MS;
|
|
// TODO: This is meant to mimic the train model or JND but this
|
|
// is a per component value. It should really be since the start
|
|
// of the total render or last commit. Consider using something like
|
|
// globalMostRecentFallbackTime. That doesn't account for being
|
|
// suspended for part of the time or when it's a new render.
|
|
// It should probably use a global start time value instead.
|
|
}
|
|
// Pop a row.
|
|
const next = renderState.tail;
|
|
renderState.rendering = next;
|
|
renderState.tail = next.sibling;
|
|
renderState.lastEffect = workInProgress.lastEffect;
|
|
renderState.renderingStartTime = now();
|
|
next.sibling = null;
|
|
|
|
// Restore the context.
|
|
// TODO: We can probably just avoid popping it instead and only
|
|
// setting it the first time we go from not suspended to suspended.
|
|
let suspenseContext = suspenseStackCursor.current;
|
|
if (didSuspendAlready) {
|
|
suspenseContext = setShallowSuspenseContext(
|
|
suspenseContext,
|
|
ForceSuspenseFallback,
|
|
);
|
|
} else {
|
|
suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
|
|
}
|
|
pushSuspenseContext(workInProgress, suspenseContext);
|
|
// Do a pass over the next row.
|
|
return next;
|
|
}
|
|
return null;
|
|
}
|
|
case FundamentalComponent: {
|
|
if (enableFundamentalAPI) {
|
|
const fundamentalImpl = workInProgress.type.impl;
|
|
let fundamentalInstance: ReactFundamentalComponentInstance<
|
|
any,
|
|
any,
|
|
> | null = workInProgress.stateNode;
|
|
|
|
if (fundamentalInstance === null) {
|
|
const getInitialState = fundamentalImpl.getInitialState;
|
|
let fundamentalState;
|
|
if (getInitialState !== undefined) {
|
|
fundamentalState = getInitialState(newProps);
|
|
}
|
|
fundamentalInstance = workInProgress.stateNode = createFundamentalStateInstance(
|
|
workInProgress,
|
|
newProps,
|
|
fundamentalImpl,
|
|
fundamentalState || {},
|
|
);
|
|
const instance = ((getFundamentalComponentInstance(
|
|
fundamentalInstance,
|
|
): any): Instance);
|
|
fundamentalInstance.instance = instance;
|
|
if (fundamentalImpl.reconcileChildren === false) {
|
|
return null;
|
|
}
|
|
appendAllChildren(instance, workInProgress, false, false);
|
|
mountFundamentalComponent(fundamentalInstance);
|
|
} else {
|
|
// We fire update in commit phase
|
|
const prevProps = fundamentalInstance.props;
|
|
fundamentalInstance.prevProps = prevProps;
|
|
fundamentalInstance.props = newProps;
|
|
fundamentalInstance.currentFiber = workInProgress;
|
|
if (supportsPersistence) {
|
|
const instance = cloneFundamentalInstance(fundamentalInstance);
|
|
fundamentalInstance.instance = instance;
|
|
appendAllChildren(instance, workInProgress, false, false);
|
|
}
|
|
const shouldUpdate = shouldUpdateFundamentalComponent(
|
|
fundamentalInstance,
|
|
);
|
|
if (shouldUpdate) {
|
|
markUpdate(workInProgress);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
break;
|
|
}
|
|
case ScopeComponent: {
|
|
if (enableScopeAPI) {
|
|
if (current === null) {
|
|
const type = workInProgress.type;
|
|
const scopeInstance: ReactScopeInstance = {
|
|
fiber: workInProgress,
|
|
methods: null,
|
|
};
|
|
workInProgress.stateNode = scopeInstance;
|
|
scopeInstance.methods = createScopeMethods(type, scopeInstance);
|
|
if (enableDeprecatedFlareAPI) {
|
|
const listeners = newProps.DEPRECATED_flareListeners;
|
|
if (listeners != null) {
|
|
const rootContainerInstance = getRootHostContainer();
|
|
updateDeprecatedEventListeners(
|
|
listeners,
|
|
workInProgress,
|
|
rootContainerInstance,
|
|
);
|
|
}
|
|
}
|
|
if (workInProgress.ref !== null) {
|
|
markRef(workInProgress);
|
|
markUpdate(workInProgress);
|
|
}
|
|
} else {
|
|
if (enableDeprecatedFlareAPI) {
|
|
const prevListeners =
|
|
current.memoizedProps.DEPRECATED_flareListeners;
|
|
const nextListeners = newProps.DEPRECATED_flareListeners;
|
|
if (
|
|
prevListeners !== nextListeners ||
|
|
workInProgress.ref !== null
|
|
) {
|
|
markUpdate(workInProgress);
|
|
}
|
|
} else {
|
|
if (workInProgress.ref !== null) {
|
|
markUpdate(workInProgress);
|
|
}
|
|
}
|
|
if (current.ref !== workInProgress.ref) {
|
|
markRef(workInProgress);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
break;
|
|
}
|
|
case Block:
|
|
if (enableBlocksAPI) {
|
|
return null;
|
|
}
|
|
break;
|
|
}
|
|
invariant(
|
|
false,
|
|
'Unknown unit of work tag (%s). This error is likely caused by a bug in ' +
|
|
'React. Please file an issue.',
|
|
workInProgress.tag,
|
|
);
|
|
}
|
|
|
|
export {completeWork};
|