/** * 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 { Instance, TextInstance, Container, ChildSet, UpdatePayload, } from './ReactFiberHostConfig'; import type {Fiber} from './ReactFiber'; import type {FiberRoot} from './ReactFiberRoot'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {CapturedValue, CapturedError} from './ReactCapturedValue'; import { enableSchedulerTracing, enableProfilerTimer, enableSuspense, } from 'shared/ReactFeatureFlags'; import { ClassComponent, ClassComponentLazy, HostRoot, HostComponent, HostText, HostPortal, Profiler, PlaceholderComponent, } from 'shared/ReactWorkTags'; import { invokeGuardedCallback, hasCaughtError, clearCaughtError, } from 'shared/ReactErrorUtils'; import { NoEffect, ContentReset, Placement, Snapshot, Update, } from 'shared/ReactSideEffectTags'; import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; import {Sync} from './ReactFiberExpirationTime'; import {onCommitUnmount} from './ReactFiberDevToolsHook'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; import {getStackByFiberInDevAndProd} from './ReactCurrentFiber'; import {logCapturedError} from './ReactFiberErrorLogger'; import {getCommitTime} from './ReactProfilerTimer'; import {commitUpdateQueue} from './ReactUpdateQueue'; import { getPublicInstance, supportsMutation, supportsPersistence, commitMount, commitUpdate, resetTextContent, commitTextUpdate, appendChild, appendChildToContainer, insertBefore, insertInContainerBefore, removeChild, removeChildFromContainer, replaceContainerChildren, createContainerChildSet, } from './ReactFiberHostConfig'; import { captureCommitPhaseError, requestCurrentTime, scheduleWork, } from './ReactFiberScheduler'; import {StrictMode} from './ReactTypeOfMode'; const emptyObject = {}; let didWarnAboutUndefinedSnapshotBeforeUpdate: Set | null = null; if (__DEV__) { didWarnAboutUndefinedSnapshotBeforeUpdate = new Set(); } export function logError(boundary: Fiber, errorInfo: CapturedValue) { const source = errorInfo.source; let stack = errorInfo.stack; if (stack === null && source !== null) { stack = getStackByFiberInDevAndProd(source); } const capturedError: CapturedError = { componentName: source !== null ? getComponentName(source.type) : null, componentStack: stack !== null ? stack : '', error: errorInfo.value, errorBoundary: null, errorBoundaryName: null, errorBoundaryFound: false, willRetry: false, }; if (boundary !== null && boundary.tag === ClassComponent) { capturedError.errorBoundary = boundary.stateNode; capturedError.errorBoundaryName = getComponentName(boundary.type); capturedError.errorBoundaryFound = true; capturedError.willRetry = true; } try { logCapturedError(capturedError); } catch (e) { // This method must not throw, or React internal state will get messed up. // If console.error is overridden, or logCapturedError() shows a dialog that throws, // we want to report this error outside of the normal stack as a last resort. // https://github.com/facebook/react/issues/13188 setTimeout(() => { throw e; }); } } const callComponentWillUnmountWithTimer = function(current, instance) { startPhaseTimer(current, 'componentWillUnmount'); instance.props = current.memoizedProps; instance.state = current.memoizedState; instance.componentWillUnmount(); stopPhaseTimer(); }; // Capture errors so they don't interrupt unmounting. function safelyCallComponentWillUnmount(current, instance) { if (__DEV__) { invokeGuardedCallback( null, callComponentWillUnmountWithTimer, null, current, instance, ); if (hasCaughtError()) { const unmountError = clearCaughtError(); captureCommitPhaseError(current, unmountError); } } else { try { callComponentWillUnmountWithTimer(current, instance); } catch (unmountError) { captureCommitPhaseError(current, unmountError); } } } function safelyDetachRef(current: Fiber) { const ref = current.ref; if (ref !== null) { if (typeof ref === 'function') { if (__DEV__) { invokeGuardedCallback(null, ref, null, null); if (hasCaughtError()) { const refError = clearCaughtError(); captureCommitPhaseError(current, refError); } } else { try { ref(null); } catch (refError) { captureCommitPhaseError(current, refError); } } } else { ref.current = null; } } } function commitBeforeMutationLifeCycles( current: Fiber | null, finishedWork: Fiber, ): void { switch (finishedWork.tag) { case ClassComponent: case ClassComponentLazy: { if (finishedWork.effectTag & Snapshot) { if (current !== null) { const prevProps = current.memoizedProps; const prevState = current.memoizedState; startPhaseTimer(finishedWork, 'getSnapshotBeforeUpdate'); const instance = finishedWork.stateNode; instance.props = finishedWork.memoizedProps; instance.state = finishedWork.memoizedState; const snapshot = instance.getSnapshotBeforeUpdate( prevProps, prevState, ); if (__DEV__) { const didWarnSet = ((didWarnAboutUndefinedSnapshotBeforeUpdate: any): Set< mixed, >); if (snapshot === undefined && !didWarnSet.has(finishedWork.type)) { didWarnSet.add(finishedWork.type); warningWithoutStack( false, '%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' + 'must be returned. You have returned undefined.', getComponentName(finishedWork.type), ); } } instance.__reactInternalSnapshotBeforeUpdate = snapshot; stopPhaseTimer(); } } return; } case HostRoot: case HostComponent: case HostText: case HostPortal: // Nothing to do for these component types return; default: { invariant( false, 'This unit of work tag should not have side-effects. This error is ' + 'likely caused by a bug in React. Please file an issue.', ); } } } function commitLifeCycles( finishedRoot: FiberRoot, current: Fiber | null, finishedWork: Fiber, committedExpirationTime: ExpirationTime, ): void { switch (finishedWork.tag) { case ClassComponent: case ClassComponentLazy: { const instance = finishedWork.stateNode; if (finishedWork.effectTag & Update) { if (current === null) { startPhaseTimer(finishedWork, 'componentDidMount'); instance.props = finishedWork.memoizedProps; instance.state = finishedWork.memoizedState; instance.componentDidMount(); stopPhaseTimer(); } else { const prevProps = current.memoizedProps; const prevState = current.memoizedState; startPhaseTimer(finishedWork, 'componentDidUpdate'); instance.props = finishedWork.memoizedProps; instance.state = finishedWork.memoizedState; instance.componentDidUpdate( prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate, ); stopPhaseTimer(); } } const updateQueue = finishedWork.updateQueue; if (updateQueue !== null) { instance.props = finishedWork.memoizedProps; instance.state = finishedWork.memoizedState; commitUpdateQueue( finishedWork, updateQueue, instance, committedExpirationTime, ); } return; } case HostRoot: { const updateQueue = finishedWork.updateQueue; if (updateQueue !== null) { let instance = null; if (finishedWork.child !== null) { switch (finishedWork.child.tag) { case HostComponent: instance = getPublicInstance(finishedWork.child.stateNode); break; case ClassComponent: case ClassComponentLazy: instance = finishedWork.child.stateNode; break; } } commitUpdateQueue( finishedWork, updateQueue, instance, committedExpirationTime, ); } return; } case HostComponent: { const instance: Instance = finishedWork.stateNode; // Renderers may schedule work to be done after host components are mounted // (eg DOM renderer may schedule auto-focus for inputs and form controls). // These effects should only be committed when components are first mounted, // aka when there is no current/alternate. if (current === null && finishedWork.effectTag & Update) { const type = finishedWork.type; const props = finishedWork.memoizedProps; commitMount(instance, type, props, finishedWork); } return; } case HostText: { // We have no life-cycles associated with text. return; } case HostPortal: { // We have no life-cycles associated with portals. return; } case Profiler: { if (enableProfilerTimer) { const onRender = finishedWork.memoizedProps.onRender; if (enableSchedulerTracing) { onRender( finishedWork.memoizedProps.id, current === null ? 'mount' : 'update', finishedWork.actualDuration, finishedWork.treeBaseDuration, finishedWork.actualStartTime, getCommitTime(), finishedRoot.memoizedInteractions, ); } else { onRender( finishedWork.memoizedProps.id, current === null ? 'mount' : 'update', finishedWork.actualDuration, finishedWork.treeBaseDuration, finishedWork.actualStartTime, getCommitTime(), ); } } return; } case PlaceholderComponent: { if (enableSuspense) { if ((finishedWork.mode & StrictMode) === NoEffect) { // In loose mode, a placeholder times out by scheduling a synchronous // update in the commit phase. Use `updateQueue` field to signal that // the Timeout needs to switch to the placeholder. We don't need an // entire queue. Any non-null value works. // $FlowFixMe - Intentionally using a value other than an UpdateQueue. finishedWork.updateQueue = emptyObject; scheduleWork(finishedWork, Sync); } else { // In strict mode, the Update effect is used to record the time at // which the placeholder timed out. const currentTime = requestCurrentTime(); finishedWork.stateNode = {timedOutAt: currentTime}; } } return; } default: { invariant( false, 'This unit of work tag should not have side-effects. This error is ' + 'likely caused by a bug in React. Please file an issue.', ); } } } function commitAttachRef(finishedWork: Fiber) { const ref = finishedWork.ref; if (ref !== null) { const instance = finishedWork.stateNode; let instanceToUse; switch (finishedWork.tag) { case HostComponent: instanceToUse = getPublicInstance(instance); break; default: instanceToUse = instance; } if (typeof ref === 'function') { ref(instanceToUse); } else { if (__DEV__) { if (!ref.hasOwnProperty('current')) { warningWithoutStack( false, 'Unexpected ref object provided for %s. ' + 'Use either a ref-setter function or React.createRef().%s', getComponentName(finishedWork.type), getStackByFiberInDevAndProd(finishedWork), ); } } ref.current = instanceToUse; } } } function commitDetachRef(current: Fiber) { const currentRef = current.ref; if (currentRef !== null) { if (typeof currentRef === 'function') { currentRef(null); } else { currentRef.current = null; } } } // User-originating errors (lifecycles and refs) should not interrupt // deletion, so don't let them throw. Host-originating errors should // interrupt deletion, so it's okay function commitUnmount(current: Fiber): void { onCommitUnmount(current); switch (current.tag) { case ClassComponent: case ClassComponentLazy: { safelyDetachRef(current); const instance = current.stateNode; if (typeof instance.componentWillUnmount === 'function') { safelyCallComponentWillUnmount(current, instance); } return; } case HostComponent: { safelyDetachRef(current); return; } case HostPortal: { // TODO: this is recursive. // We are also not using this parent because // the portal will get pushed immediately. if (supportsMutation) { unmountHostComponents(current); } else if (supportsPersistence) { emptyPortalContainer(current); } return; } } } function commitNestedUnmounts(root: Fiber): void { // While we're inside a removed host node we don't want to call // removeChild on the inner nodes because they're removed by the top // call anyway. We also want to call componentWillUnmount on all // composites before this host node is removed from the tree. Therefore // we do an inner loop while we're still inside the host node. let node: Fiber = root; while (true) { commitUnmount(node); // Visit children because they may contain more composite or host nodes. // Skip portals because commitUnmount() currently visits them recursively. if ( node.child !== null && // If we use mutation we drill down into portals using commitUnmount above. // If we don't use mutation we drill down into portals here instead. (!supportsMutation || node.tag !== HostPortal) ) { node.child.return = node; node = node.child; continue; } if (node === root) { return; } while (node.sibling === null) { if (node.return === null || node.return === root) { return; } node = node.return; } node.sibling.return = node.return; node = node.sibling; } } function detachFiber(current: Fiber) { // Cut off the return pointers to disconnect it from the tree. Ideally, we // should clear the child pointer of the parent alternate to let this // get GC:ed but we don't know which for sure which parent is the current // one so we'll settle for GC:ing the subtree of this child. This child // itself will be GC:ed when the parent updates the next time. current.return = null; current.child = null; if (current.alternate) { current.alternate.child = null; current.alternate.return = null; } } function emptyPortalContainer(current: Fiber) { if (!supportsPersistence) { return; } const portal: {containerInfo: Container, pendingChildren: ChildSet} = current.stateNode; const {containerInfo} = portal; const emptyChildSet = createContainerChildSet(containerInfo); replaceContainerChildren(containerInfo, emptyChildSet); } function commitContainer(finishedWork: Fiber) { if (!supportsPersistence) { return; } switch (finishedWork.tag) { case ClassComponent: case ClassComponentLazy: { return; } case HostComponent: { return; } case HostText: { return; } case HostRoot: case HostPortal: { const portalOrRoot: { containerInfo: Container, pendingChildren: ChildSet, } = finishedWork.stateNode; const {containerInfo, pendingChildren} = portalOrRoot; replaceContainerChildren(containerInfo, pendingChildren); return; } default: { invariant( false, 'This unit of work tag should not have side-effects. This error is ' + 'likely caused by a bug in React. Please file an issue.', ); } } } function getHostParentFiber(fiber: Fiber): Fiber { let parent = fiber.return; while (parent !== null) { if (isHostParent(parent)) { return parent; } parent = parent.return; } invariant( false, 'Expected to find a host parent. This error is likely caused by a bug ' + 'in React. Please file an issue.', ); } function isHostParent(fiber: Fiber): boolean { return ( fiber.tag === HostComponent || fiber.tag === HostRoot || fiber.tag === HostPortal ); } function getHostSibling(fiber: Fiber): ?Instance { // We're going to search forward into the tree until we find a sibling host // node. Unfortunately, if multiple insertions are done in a row we have to // search past them. This leads to exponential search for the next sibling. // TODO: Find a more efficient way to do this. let node: Fiber = fiber; siblings: while (true) { // If we didn't find anything, let's try the next sibling. while (node.sibling === null) { if (node.return === null || isHostParent(node.return)) { // If we pop out of the root or hit the parent the fiber we are the // last sibling. return null; } node = node.return; } node.sibling.return = node.return; node = node.sibling; while (node.tag !== HostComponent && node.tag !== HostText) { // If it is not host node and, we might have a host node inside it. // Try to search down until we find one. if (node.effectTag & Placement) { // If we don't have a child, try the siblings instead. continue siblings; } // If we don't have a child, try the siblings instead. // We also skip portals because they are not part of this host tree. if (node.child === null || node.tag === HostPortal) { continue siblings; } else { node.child.return = node; node = node.child; } } // Check if this host node is stable or about to be placed. if (!(node.effectTag & Placement)) { // Found it! return node.stateNode; } } } function commitPlacement(finishedWork: Fiber): void { if (!supportsMutation) { return; } // Recursively insert all host nodes into the parent. const parentFiber = getHostParentFiber(finishedWork); // Note: these two variables *must* always be updated together. let parent; let isContainer; switch (parentFiber.tag) { case HostComponent: parent = parentFiber.stateNode; isContainer = false; break; case HostRoot: parent = parentFiber.stateNode.containerInfo; isContainer = true; break; case HostPortal: parent = parentFiber.stateNode.containerInfo; isContainer = true; break; default: invariant( false, 'Invalid host parent fiber. This error is likely caused by a bug ' + 'in React. Please file an issue.', ); } if (parentFiber.effectTag & ContentReset) { // Reset the text content of the parent before doing any insertions resetTextContent(parent); // Clear ContentReset from the effect tag parentFiber.effectTag &= ~ContentReset; } const before = getHostSibling(finishedWork); // We only have the top Fiber that was inserted but we need recurse down its // children to find all the terminal nodes. let node: Fiber = finishedWork; while (true) { if (node.tag === HostComponent || node.tag === HostText) { if (before) { if (isContainer) { insertInContainerBefore(parent, node.stateNode, before); } else { insertBefore(parent, node.stateNode, before); } } else { if (isContainer) { appendChildToContainer(parent, node.stateNode); } else { appendChild(parent, node.stateNode); } } } else if (node.tag === HostPortal) { // If the insertion itself is a portal, 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 === finishedWork) { return; } while (node.sibling === null) { if (node.return === null || node.return === finishedWork) { return; } node = node.return; } node.sibling.return = node.return; node = node.sibling; } } function unmountHostComponents(current): void { // We only have the top Fiber that was deleted but we need recurse down its // children to find all the terminal nodes. let node: Fiber = current; // Each iteration, currentParent is populated with node's host parent if not // currentParentIsValid. let currentParentIsValid = false; // Note: these two variables *must* always be updated together. let currentParent; let currentParentIsContainer; while (true) { if (!currentParentIsValid) { let parent = node.return; findParent: while (true) { invariant( parent !== null, 'Expected to find a host parent. This error is likely caused by ' + 'a bug in React. Please file an issue.', ); switch (parent.tag) { case HostComponent: currentParent = parent.stateNode; currentParentIsContainer = false; break findParent; case HostRoot: currentParent = parent.stateNode.containerInfo; currentParentIsContainer = true; break findParent; case HostPortal: currentParent = parent.stateNode.containerInfo; currentParentIsContainer = true; break findParent; } parent = parent.return; } currentParentIsValid = true; } if (node.tag === HostComponent || node.tag === HostText) { commitNestedUnmounts(node); // After all the children have unmounted, it is now safe to remove the // node from the tree. if (currentParentIsContainer) { removeChildFromContainer((currentParent: any), node.stateNode); } else { removeChild((currentParent: any), node.stateNode); } // Don't visit children because we already visited them. } else if (node.tag === HostPortal) { // When we go into a portal, it becomes the parent to remove from. // We will reassign it back when we pop the portal on the way up. currentParent = node.stateNode.containerInfo; currentParentIsContainer = true; // Visit children because portals might contain host components. if (node.child !== null) { node.child.return = node; node = node.child; continue; } } else { commitUnmount(node); // Visit children because we may find more host components below. if (node.child !== null) { node.child.return = node; node = node.child; continue; } } if (node === current) { return; } while (node.sibling === null) { if (node.return === null || node.return === current) { return; } node = node.return; if (node.tag === HostPortal) { // When we go out of the portal, we need to restore the parent. // Since we don't keep a stack of them, we will search for it. currentParentIsValid = false; } } node.sibling.return = node.return; node = node.sibling; } } function commitDeletion(current: Fiber): void { if (supportsMutation) { // Recursively delete all host nodes from the parent. // Detach refs and call componentWillUnmount() on the whole subtree. unmountHostComponents(current); } else { // Detach refs and call componentWillUnmount() on the whole subtree. commitNestedUnmounts(current); } detachFiber(current); } function commitWork(current: Fiber | null, finishedWork: Fiber): void { if (!supportsMutation) { commitContainer(finishedWork); return; } switch (finishedWork.tag) { case ClassComponent: case ClassComponentLazy: { return; } case HostComponent: { const instance: Instance = finishedWork.stateNode; if (instance != null) { // Commit the work prepared earlier. const newProps = finishedWork.memoizedProps; // For hydration we reuse the update path but we treat the oldProps // as the newProps. The updatePayload will contain the real change in // this case. const oldProps = current !== null ? current.memoizedProps : newProps; const type = finishedWork.type; // TODO: Type the updateQueue to be specific to host components. const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any); finishedWork.updateQueue = null; if (updatePayload !== null) { commitUpdate( instance, updatePayload, type, oldProps, newProps, finishedWork, ); } } return; } case HostText: { invariant( finishedWork.stateNode !== null, 'This should have a text node initialized. This error is likely ' + 'caused by a bug in React. Please file an issue.', ); const textInstance: TextInstance = finishedWork.stateNode; const newText: string = finishedWork.memoizedProps; // For hydration we reuse the update path but we treat the oldProps // as the newProps. The updatePayload will contain the real change in // this case. const oldText: string = current !== null ? current.memoizedProps : newText; commitTextUpdate(textInstance, oldText, newText); return; } case HostRoot: { return; } case Profiler: { return; } case PlaceholderComponent: { return; } default: { invariant( false, 'This unit of work tag should not have side-effects. This error is ' + 'likely caused by a bug in React. Please file an issue.', ); } } } function commitResetTextContent(current: Fiber) { if (!supportsMutation) { return; } resetTextContent(current.stateNode); } export { commitBeforeMutationLifeCycles, commitResetTextContent, commitPlacement, commitDeletion, commitWork, commitLifeCycles, commitAttachRef, commitDetachRef, };