diff --git a/packages/react-reconciler/src/ReactChildFiber.new.js b/packages/react-reconciler/src/ReactChildFiber.new.js new file mode 100644 index 0000000000000..3034c4c69e923 --- /dev/null +++ b/packages/react-reconciler/src/ReactChildFiber.new.js @@ -0,0 +1,1490 @@ +/** + * 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 {ReactElement} from 'shared/ReactElementType'; +import type {ReactPortal} from 'shared/ReactTypes'; +import type {BlockComponent} from 'react/src/ReactBlock'; +import type {LazyComponent} from 'react/src/ReactLazy'; +import type {Fiber} from './ReactInternalTypes'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; + +import getComponentName from 'shared/getComponentName'; +import {Placement, Deletion} from './ReactSideEffectTags'; +import { + getIteratorFn, + REACT_ELEMENT_TYPE, + REACT_FRAGMENT_TYPE, + REACT_PORTAL_TYPE, + REACT_LAZY_TYPE, + REACT_BLOCK_TYPE, +} from 'shared/ReactSymbols'; +import { + FunctionComponent, + ClassComponent, + HostText, + HostPortal, + Fragment, + Block, +} from './ReactWorkTags'; +import invariant from 'shared/invariant'; +import {warnAboutStringRefs, enableBlocksAPI} from 'shared/ReactFeatureFlags'; + +import { + createWorkInProgress, + resetWorkInProgress, + createFiberFromElement, + createFiberFromFragment, + createFiberFromText, + createFiberFromPortal, +} from './ReactFiber.new'; +import {emptyRefsObject} from './ReactFiberClassComponent.new'; +import {getStackByFiberInDevAndProd} from './ReactFiberComponentStack'; +import {getCurrentFiberStackInDev} from './ReactCurrentFiber'; +import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading.new'; +import {StrictMode} from './ReactTypeOfMode'; + +let didWarnAboutMaps; +let didWarnAboutGenerators; +let didWarnAboutStringRefs; +let ownerHasKeyUseWarning; +let ownerHasFunctionTypeWarning; +let warnForMissingKey = (child: mixed) => {}; + +if (__DEV__) { + didWarnAboutMaps = false; + didWarnAboutGenerators = false; + didWarnAboutStringRefs = {}; + + /** + * Warn if there's no key explicitly set on dynamic arrays of children or + * object keys are not valid. This allows us to keep track of children between + * updates. + */ + ownerHasKeyUseWarning = {}; + ownerHasFunctionTypeWarning = {}; + + warnForMissingKey = (child: mixed) => { + if (child === null || typeof child !== 'object') { + return; + } + if (!child._store || child._store.validated || child.key != null) { + return; + } + invariant( + typeof child._store === 'object', + 'React Component in warnForMissingKey should have a _store. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + child._store.validated = true; + + const currentComponentErrorInfo = + 'Each child in a list should have a unique ' + + '"key" prop. See https://fb.me/react-warning-keys for ' + + 'more information.' + + getCurrentFiberStackInDev(); + if (ownerHasKeyUseWarning[currentComponentErrorInfo]) { + return; + } + ownerHasKeyUseWarning[currentComponentErrorInfo] = true; + + console.error( + 'Each child in a list should have a unique ' + + '"key" prop. See https://fb.me/react-warning-keys for ' + + 'more information.', + ); + }; +} + +const isArray = Array.isArray; + +function coerceRef( + returnFiber: Fiber, + current: Fiber | null, + element: ReactElement, +) { + const mixedRef = element.ref; + if ( + mixedRef !== null && + typeof mixedRef !== 'function' && + typeof mixedRef !== 'object' + ) { + if (__DEV__) { + // TODO: Clean this up once we turn on the string ref warning for + // everyone, because the strict mode case will no longer be relevant + if ( + (returnFiber.mode & StrictMode || warnAboutStringRefs) && + // We warn in ReactElement.js if owner and self are equal for string refs + // because these cannot be automatically converted to an arrow function + // using a codemod. Therefore, we don't have to warn about string refs again. + !( + element._owner && + element._self && + element._owner.stateNode !== element._self + ) + ) { + const componentName = getComponentName(returnFiber.type) || 'Component'; + if (!didWarnAboutStringRefs[componentName]) { + if (warnAboutStringRefs) { + console.error( + 'Component "%s" contains the string ref "%s". Support for string refs ' + + 'will be removed in a future major release. We recommend using ' + + 'useRef() or createRef() instead. ' + + 'Learn more about using refs safely here: ' + + 'https://fb.me/react-strict-mode-string-ref%s', + componentName, + mixedRef, + getStackByFiberInDevAndProd(returnFiber), + ); + } else { + console.error( + 'A string ref, "%s", has been found within a strict mode tree. ' + + 'String refs are a source of potential bugs and should be avoided. ' + + 'We recommend using useRef() or createRef() instead. ' + + 'Learn more about using refs safely here: ' + + 'https://fb.me/react-strict-mode-string-ref%s', + mixedRef, + getStackByFiberInDevAndProd(returnFiber), + ); + } + didWarnAboutStringRefs[componentName] = true; + } + } + } + + if (element._owner) { + const owner: ?Fiber = (element._owner: any); + let inst; + if (owner) { + const ownerFiber = ((owner: any): Fiber); + invariant( + ownerFiber.tag === ClassComponent, + 'Function components cannot have string refs. ' + + 'We recommend using useRef() instead. ' + + 'Learn more about using refs safely here: ' + + 'https://fb.me/react-strict-mode-string-ref', + ); + inst = ownerFiber.stateNode; + } + invariant( + inst, + 'Missing owner for string ref %s. This error is likely caused by a ' + + 'bug in React. Please file an issue.', + mixedRef, + ); + const stringRef = '' + mixedRef; + // Check if previous string ref matches new string ref + if ( + current !== null && + current.ref !== null && + typeof current.ref === 'function' && + current.ref._stringRef === stringRef + ) { + return current.ref; + } + const ref = function(value) { + let refs = inst.refs; + if (refs === emptyRefsObject) { + // This is a lazy pooled frozen object, so we need to initialize. + refs = inst.refs = {}; + } + if (value === null) { + delete refs[stringRef]; + } else { + refs[stringRef] = value; + } + }; + ref._stringRef = stringRef; + return ref; + } else { + invariant( + typeof mixedRef === 'string', + 'Expected ref to be a function, a string, an object returned by React.createRef(), or null.', + ); + invariant( + element._owner, + 'Element ref was specified as a string (%s) but no owner was set. This could happen for one of' + + ' the following reasons:\n' + + '1. You may be adding a ref to a function component\n' + + "2. You may be adding a ref to a component that was not created inside a component's render method\n" + + '3. You have multiple copies of React loaded\n' + + 'See https://fb.me/react-refs-must-have-owner for more information.', + mixedRef, + ); + } + } + return mixedRef; +} + +function throwOnInvalidObjectType(returnFiber: Fiber, newChild: Object) { + if (returnFiber.type !== 'textarea') { + let addendum = ''; + if (__DEV__) { + addendum = + ' If you meant to render a collection of children, use an array ' + + 'instead.' + + getCurrentFiberStackInDev(); + } + invariant( + false, + 'Objects are not valid as a React child (found: %s).%s', + Object.prototype.toString.call(newChild) === '[object Object]' + ? 'object with keys {' + Object.keys(newChild).join(', ') + '}' + : newChild, + addendum, + ); + } +} + +function warnOnFunctionType() { + if (__DEV__) { + const currentComponentErrorInfo = + 'Functions are not valid as a React child. This may happen if ' + + 'you return a Component instead of from render. ' + + 'Or maybe you meant to call this function rather than return it.' + + getCurrentFiberStackInDev(); + + if (ownerHasFunctionTypeWarning[currentComponentErrorInfo]) { + return; + } + ownerHasFunctionTypeWarning[currentComponentErrorInfo] = true; + + console.error( + 'Functions are not valid as a React child. This may happen if ' + + 'you return a Component instead of from render. ' + + 'Or maybe you meant to call this function rather than return it.', + ); + } +} + +// We avoid inlining this to avoid potential deopts from using try/catch. +/** @noinline */ +function resolveLazyType( + lazyComponent: LazyComponent, +): LazyComponent | T { + try { + // If we can, let's peek at the resulting type. + const payload = lazyComponent._payload; + const init = lazyComponent._init; + return init(payload); + } catch (x) { + // Leave it in place and let it throw again in the begin phase. + return lazyComponent; + } +} + +// This wrapper function exists because I expect to clone the code in each path +// to be able to optimize each path individually by branching early. This needs +// a compiler or we can do it manually. Helpers that don't need this branching +// live outside of this function. +function ChildReconciler(shouldTrackSideEffects) { + function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void { + if (!shouldTrackSideEffects) { + // Noop. + return; + } + // Deletions are added in reversed order so we add it to the front. + // At this point, the return fiber's effect list is empty except for + // deletions, so we can just append the deletion to the list. The remaining + // effects aren't added until the complete phase. Once we implement + // resuming, this may not be true. + const last = returnFiber.lastEffect; + if (last !== null) { + last.nextEffect = childToDelete; + returnFiber.lastEffect = childToDelete; + } else { + returnFiber.firstEffect = returnFiber.lastEffect = childToDelete; + } + childToDelete.nextEffect = null; + childToDelete.effectTag = Deletion; + } + + function deleteRemainingChildren( + returnFiber: Fiber, + currentFirstChild: Fiber | null, + ): null { + if (!shouldTrackSideEffects) { + // Noop. + return null; + } + + // TODO: For the shouldClone case, this could be micro-optimized a bit by + // assuming that after the first child we've already added everything. + let childToDelete = currentFirstChild; + while (childToDelete !== null) { + deleteChild(returnFiber, childToDelete); + childToDelete = childToDelete.sibling; + } + return null; + } + + function mapRemainingChildren( + returnFiber: Fiber, + currentFirstChild: Fiber, + ): Map { + // Add the remaining children to a temporary map so that we can find them by + // keys quickly. Implicit (null) keys get added to this set with their index + // instead. + const existingChildren: Map = new Map(); + + let existingChild = currentFirstChild; + while (existingChild !== null) { + if (existingChild.key !== null) { + existingChildren.set(existingChild.key, existingChild); + } else { + existingChildren.set(existingChild.index, existingChild); + } + existingChild = existingChild.sibling; + } + return existingChildren; + } + + function useFiber(fiber: Fiber, pendingProps: mixed): Fiber { + // We currently set sibling to null and index to 0 here because it is easy + // to forget to do before returning it. E.g. for the single child case. + const clone = createWorkInProgress(fiber, pendingProps); + clone.index = 0; + clone.sibling = null; + return clone; + } + + function placeChild( + newFiber: Fiber, + lastPlacedIndex: number, + newIndex: number, + ): number { + newFiber.index = newIndex; + if (!shouldTrackSideEffects) { + // Noop. + return lastPlacedIndex; + } + const current = newFiber.alternate; + if (current !== null) { + const oldIndex = current.index; + if (oldIndex < lastPlacedIndex) { + // This is a move. + newFiber.effectTag = Placement; + return lastPlacedIndex; + } else { + // This item can stay in place. + return oldIndex; + } + } else { + // This is an insertion. + newFiber.effectTag = Placement; + return lastPlacedIndex; + } + } + + function placeSingleChild(newFiber: Fiber): Fiber { + // This is simpler for the single child case. We only need to do a + // placement for inserting new children. + if (shouldTrackSideEffects && newFiber.alternate === null) { + newFiber.effectTag = Placement; + } + return newFiber; + } + + function updateTextNode( + returnFiber: Fiber, + current: Fiber | null, + textContent: string, + expirationTime: ExpirationTime, + ) { + if (current === null || current.tag !== HostText) { + // Insert + const created = createFiberFromText( + textContent, + returnFiber.mode, + expirationTime, + ); + created.return = returnFiber; + return created; + } else { + // Update + const existing = useFiber(current, textContent); + existing.return = returnFiber; + return existing; + } + } + + function updateElement( + returnFiber: Fiber, + current: Fiber | null, + element: ReactElement, + expirationTime: ExpirationTime, + ): Fiber { + if (current !== null) { + if ( + current.elementType === element.type || + // Keep this check inline so it only runs on the false path: + (__DEV__ ? isCompatibleFamilyForHotReloading(current, element) : false) + ) { + // Move based on index + const existing = useFiber(current, element.props); + existing.ref = coerceRef(returnFiber, current, element); + existing.return = returnFiber; + if (__DEV__) { + existing._debugSource = element._source; + existing._debugOwner = element._owner; + } + return existing; + } else if (enableBlocksAPI && current.tag === Block) { + // The new Block might not be initialized yet. We need to initialize + // it in case initializing it turns out it would match. + let type = element.type; + if (type.$$typeof === REACT_LAZY_TYPE) { + type = resolveLazyType(type); + } + if ( + type.$$typeof === REACT_BLOCK_TYPE && + ((type: any): BlockComponent)._render === + (current.type: BlockComponent)._render + ) { + // Same as above but also update the .type field. + const existing = useFiber(current, element.props); + existing.return = returnFiber; + existing.type = type; + if (__DEV__) { + existing._debugSource = element._source; + existing._debugOwner = element._owner; + } + return existing; + } + } + } + // Insert + const created = createFiberFromElement( + element, + returnFiber.mode, + expirationTime, + ); + created.ref = coerceRef(returnFiber, current, element); + created.return = returnFiber; + return created; + } + + function updatePortal( + returnFiber: Fiber, + current: Fiber | null, + portal: ReactPortal, + expirationTime: ExpirationTime, + ): Fiber { + if ( + current === null || + current.tag !== HostPortal || + current.stateNode.containerInfo !== portal.containerInfo || + current.stateNode.implementation !== portal.implementation + ) { + // Insert + const created = createFiberFromPortal( + portal, + returnFiber.mode, + expirationTime, + ); + created.return = returnFiber; + return created; + } else { + // Update + const existing = useFiber(current, portal.children || []); + existing.return = returnFiber; + return existing; + } + } + + function updateFragment( + returnFiber: Fiber, + current: Fiber | null, + fragment: Iterable<*>, + expirationTime: ExpirationTime, + key: null | string, + ): Fiber { + if (current === null || current.tag !== Fragment) { + // Insert + const created = createFiberFromFragment( + fragment, + returnFiber.mode, + expirationTime, + key, + ); + created.return = returnFiber; + return created; + } else { + // Update + const existing = useFiber(current, fragment); + existing.return = returnFiber; + return existing; + } + } + + function createChild( + returnFiber: Fiber, + newChild: any, + expirationTime: ExpirationTime, + ): Fiber | null { + if (typeof newChild === 'string' || typeof newChild === 'number') { + // Text nodes don't have keys. If the previous node is implicitly keyed + // we can continue to replace it without aborting even if it is not a text + // node. + const created = createFiberFromText( + '' + newChild, + returnFiber.mode, + expirationTime, + ); + created.return = returnFiber; + return created; + } + + if (typeof newChild === 'object' && newChild !== null) { + switch (newChild.$$typeof) { + case REACT_ELEMENT_TYPE: { + const created = createFiberFromElement( + newChild, + returnFiber.mode, + expirationTime, + ); + created.ref = coerceRef(returnFiber, null, newChild); + created.return = returnFiber; + return created; + } + case REACT_PORTAL_TYPE: { + const created = createFiberFromPortal( + newChild, + returnFiber.mode, + expirationTime, + ); + created.return = returnFiber; + return created; + } + } + + if (isArray(newChild) || getIteratorFn(newChild)) { + const created = createFiberFromFragment( + newChild, + returnFiber.mode, + expirationTime, + null, + ); + created.return = returnFiber; + return created; + } + + throwOnInvalidObjectType(returnFiber, newChild); + } + + if (__DEV__) { + if (typeof newChild === 'function') { + warnOnFunctionType(); + } + } + + return null; + } + + function updateSlot( + returnFiber: Fiber, + oldFiber: Fiber | null, + newChild: any, + expirationTime: ExpirationTime, + ): Fiber | null { + // Update the fiber if the keys match, otherwise return null. + + const key = oldFiber !== null ? oldFiber.key : null; + + if (typeof newChild === 'string' || typeof newChild === 'number') { + // Text nodes don't have keys. If the previous node is implicitly keyed + // we can continue to replace it without aborting even if it is not a text + // node. + if (key !== null) { + return null; + } + return updateTextNode( + returnFiber, + oldFiber, + '' + newChild, + expirationTime, + ); + } + + if (typeof newChild === 'object' && newChild !== null) { + switch (newChild.$$typeof) { + case REACT_ELEMENT_TYPE: { + if (newChild.key === key) { + if (newChild.type === REACT_FRAGMENT_TYPE) { + return updateFragment( + returnFiber, + oldFiber, + newChild.props.children, + expirationTime, + key, + ); + } + return updateElement( + returnFiber, + oldFiber, + newChild, + expirationTime, + ); + } else { + return null; + } + } + case REACT_PORTAL_TYPE: { + if (newChild.key === key) { + return updatePortal( + returnFiber, + oldFiber, + newChild, + expirationTime, + ); + } else { + return null; + } + } + } + + if (isArray(newChild) || getIteratorFn(newChild)) { + if (key !== null) { + return null; + } + + return updateFragment( + returnFiber, + oldFiber, + newChild, + expirationTime, + null, + ); + } + + throwOnInvalidObjectType(returnFiber, newChild); + } + + if (__DEV__) { + if (typeof newChild === 'function') { + warnOnFunctionType(); + } + } + + return null; + } + + function updateFromMap( + existingChildren: Map, + returnFiber: Fiber, + newIdx: number, + newChild: any, + expirationTime: ExpirationTime, + ): Fiber | null { + if (typeof newChild === 'string' || typeof newChild === 'number') { + // Text nodes don't have keys, so we neither have to check the old nor + // new node for the key. If both are text nodes, they match. + const matchedFiber = existingChildren.get(newIdx) || null; + return updateTextNode( + returnFiber, + matchedFiber, + '' + newChild, + expirationTime, + ); + } + + if (typeof newChild === 'object' && newChild !== null) { + switch (newChild.$$typeof) { + case REACT_ELEMENT_TYPE: { + const matchedFiber = + existingChildren.get( + newChild.key === null ? newIdx : newChild.key, + ) || null; + if (newChild.type === REACT_FRAGMENT_TYPE) { + return updateFragment( + returnFiber, + matchedFiber, + newChild.props.children, + expirationTime, + newChild.key, + ); + } + return updateElement( + returnFiber, + matchedFiber, + newChild, + expirationTime, + ); + } + case REACT_PORTAL_TYPE: { + const matchedFiber = + existingChildren.get( + newChild.key === null ? newIdx : newChild.key, + ) || null; + return updatePortal( + returnFiber, + matchedFiber, + newChild, + expirationTime, + ); + } + } + + if (isArray(newChild) || getIteratorFn(newChild)) { + const matchedFiber = existingChildren.get(newIdx) || null; + return updateFragment( + returnFiber, + matchedFiber, + newChild, + expirationTime, + null, + ); + } + + throwOnInvalidObjectType(returnFiber, newChild); + } + + if (__DEV__) { + if (typeof newChild === 'function') { + warnOnFunctionType(); + } + } + + return null; + } + + /** + * Warns if there is a duplicate or missing key + */ + function warnOnInvalidKey( + child: mixed, + knownKeys: Set | null, + ): Set | null { + if (__DEV__) { + if (typeof child !== 'object' || child === null) { + return knownKeys; + } + switch (child.$$typeof) { + case REACT_ELEMENT_TYPE: + case REACT_PORTAL_TYPE: + warnForMissingKey(child); + const key = child.key; + if (typeof key !== 'string') { + break; + } + if (knownKeys === null) { + knownKeys = new Set(); + knownKeys.add(key); + break; + } + if (!knownKeys.has(key)) { + knownKeys.add(key); + break; + } + console.error( + 'Encountered two children with the same key, `%s`. ' + + 'Keys should be unique so that components maintain their identity ' + + 'across updates. Non-unique keys may cause children to be ' + + 'duplicated and/or omitted — the behavior is unsupported and ' + + 'could change in a future version.', + key, + ); + break; + default: + break; + } + } + return knownKeys; + } + + function reconcileChildrenArray( + returnFiber: Fiber, + currentFirstChild: Fiber | null, + newChildren: Array<*>, + expirationTime: ExpirationTime, + ): Fiber | null { + // This algorithm can't optimize by searching from both ends since we + // don't have backpointers on fibers. I'm trying to see how far we can get + // with that model. If it ends up not being worth the tradeoffs, we can + // add it later. + + // Even with a two ended optimization, we'd want to optimize for the case + // where there are few changes and brute force the comparison instead of + // going for the Map. It'd like to explore hitting that path first in + // forward-only mode and only go for the Map once we notice that we need + // lots of look ahead. This doesn't handle reversal as well as two ended + // search but that's unusual. Besides, for the two ended optimization to + // work on Iterables, we'd need to copy the whole set. + + // In this first iteration, we'll just live with hitting the bad case + // (adding everything to a Map) in for every insert/move. + + // If you change this code, also update reconcileChildrenIterator() which + // uses the same algorithm. + + if (__DEV__) { + // First, validate keys. + let knownKeys = null; + for (let i = 0; i < newChildren.length; i++) { + const child = newChildren[i]; + knownKeys = warnOnInvalidKey(child, knownKeys); + } + } + + let resultingFirstChild: Fiber | null = null; + let previousNewFiber: Fiber | null = null; + + let oldFiber = currentFirstChild; + let lastPlacedIndex = 0; + let newIdx = 0; + let nextOldFiber = null; + for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) { + if (oldFiber.index > newIdx) { + nextOldFiber = oldFiber; + oldFiber = null; + } else { + nextOldFiber = oldFiber.sibling; + } + const newFiber = updateSlot( + returnFiber, + oldFiber, + newChildren[newIdx], + expirationTime, + ); + if (newFiber === null) { + // TODO: This breaks on empty slots like null children. That's + // unfortunate because it triggers the slow path all the time. We need + // a better way to communicate whether this was a miss or null, + // boolean, undefined, etc. + if (oldFiber === null) { + oldFiber = nextOldFiber; + } + break; + } + if (shouldTrackSideEffects) { + if (oldFiber && newFiber.alternate === null) { + // We matched the slot, but we didn't reuse the existing fiber, so we + // need to delete the existing child. + deleteChild(returnFiber, oldFiber); + } + } + lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); + if (previousNewFiber === null) { + // TODO: Move out of the loop. This only happens for the first run. + resultingFirstChild = newFiber; + } else { + // TODO: Defer siblings if we're not at the right index for this slot. + // I.e. if we had null values before, then we want to defer this + // for each null value. However, we also don't want to call updateSlot + // with the previous one. + previousNewFiber.sibling = newFiber; + } + previousNewFiber = newFiber; + oldFiber = nextOldFiber; + } + + if (newIdx === newChildren.length) { + // We've reached the end of the new children. We can delete the rest. + deleteRemainingChildren(returnFiber, oldFiber); + return resultingFirstChild; + } + + if (oldFiber === null) { + // If we don't have any more existing children we can choose a fast path + // since the rest will all be insertions. + for (; newIdx < newChildren.length; newIdx++) { + const newFiber = createChild( + returnFiber, + newChildren[newIdx], + expirationTime, + ); + if (newFiber === null) { + continue; + } + lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); + if (previousNewFiber === null) { + // TODO: Move out of the loop. This only happens for the first run. + resultingFirstChild = newFiber; + } else { + previousNewFiber.sibling = newFiber; + } + previousNewFiber = newFiber; + } + return resultingFirstChild; + } + + // Add all children to a key map for quick lookups. + const existingChildren = mapRemainingChildren(returnFiber, oldFiber); + + // Keep scanning and use the map to restore deleted items as moves. + for (; newIdx < newChildren.length; newIdx++) { + const newFiber = updateFromMap( + existingChildren, + returnFiber, + newIdx, + newChildren[newIdx], + expirationTime, + ); + if (newFiber !== null) { + if (shouldTrackSideEffects) { + if (newFiber.alternate !== null) { + // The new fiber is a work in progress, but if there exists a + // current, that means that we reused the fiber. We need to delete + // it from the child list so that we don't add it to the deletion + // list. + existingChildren.delete( + newFiber.key === null ? newIdx : newFiber.key, + ); + } + } + lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); + if (previousNewFiber === null) { + resultingFirstChild = newFiber; + } else { + previousNewFiber.sibling = newFiber; + } + previousNewFiber = newFiber; + } + } + + if (shouldTrackSideEffects) { + // Any existing children that weren't consumed above were deleted. We need + // to add them to the deletion list. + existingChildren.forEach(child => deleteChild(returnFiber, child)); + } + + return resultingFirstChild; + } + + function reconcileChildrenIterator( + returnFiber: Fiber, + currentFirstChild: Fiber | null, + newChildrenIterable: Iterable<*>, + expirationTime: ExpirationTime, + ): Fiber | null { + // This is the same implementation as reconcileChildrenArray(), + // but using the iterator instead. + + const iteratorFn = getIteratorFn(newChildrenIterable); + invariant( + typeof iteratorFn === 'function', + 'An object is not an iterable. This error is likely caused by a bug in ' + + 'React. Please file an issue.', + ); + + if (__DEV__) { + // We don't support rendering Generators because it's a mutation. + // See https://github.com/facebook/react/issues/12995 + if ( + typeof Symbol === 'function' && + // $FlowFixMe Flow doesn't know about toStringTag + newChildrenIterable[Symbol.toStringTag] === 'Generator' + ) { + if (!didWarnAboutGenerators) { + console.error( + 'Using Generators as children is unsupported and will likely yield ' + + 'unexpected results because enumerating a generator mutates it. ' + + 'You may convert it to an array with `Array.from()` or the ' + + '`[...spread]` operator before rendering. Keep in mind ' + + 'you might need to polyfill these features for older browsers.', + ); + } + didWarnAboutGenerators = true; + } + + // Warn about using Maps as children + if ((newChildrenIterable: any).entries === iteratorFn) { + if (!didWarnAboutMaps) { + console.error( + 'Using Maps as children is not supported. ' + + 'Use an array of keyed ReactElements instead.', + ); + } + didWarnAboutMaps = true; + } + + // First, validate keys. + // We'll get a different iterator later for the main pass. + const newChildren = iteratorFn.call(newChildrenIterable); + if (newChildren) { + let knownKeys = null; + let step = newChildren.next(); + for (; !step.done; step = newChildren.next()) { + const child = step.value; + knownKeys = warnOnInvalidKey(child, knownKeys); + } + } + } + + const newChildren = iteratorFn.call(newChildrenIterable); + invariant(newChildren != null, 'An iterable object provided no iterator.'); + + let resultingFirstChild: Fiber | null = null; + let previousNewFiber: Fiber | null = null; + + let oldFiber = currentFirstChild; + let lastPlacedIndex = 0; + let newIdx = 0; + let nextOldFiber = null; + + let step = newChildren.next(); + for ( + ; + oldFiber !== null && !step.done; + newIdx++, step = newChildren.next() + ) { + if (oldFiber.index > newIdx) { + nextOldFiber = oldFiber; + oldFiber = null; + } else { + nextOldFiber = oldFiber.sibling; + } + const newFiber = updateSlot( + returnFiber, + oldFiber, + step.value, + expirationTime, + ); + if (newFiber === null) { + // TODO: This breaks on empty slots like null children. That's + // unfortunate because it triggers the slow path all the time. We need + // a better way to communicate whether this was a miss or null, + // boolean, undefined, etc. + if (oldFiber === null) { + oldFiber = nextOldFiber; + } + break; + } + if (shouldTrackSideEffects) { + if (oldFiber && newFiber.alternate === null) { + // We matched the slot, but we didn't reuse the existing fiber, so we + // need to delete the existing child. + deleteChild(returnFiber, oldFiber); + } + } + lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); + if (previousNewFiber === null) { + // TODO: Move out of the loop. This only happens for the first run. + resultingFirstChild = newFiber; + } else { + // TODO: Defer siblings if we're not at the right index for this slot. + // I.e. if we had null values before, then we want to defer this + // for each null value. However, we also don't want to call updateSlot + // with the previous one. + previousNewFiber.sibling = newFiber; + } + previousNewFiber = newFiber; + oldFiber = nextOldFiber; + } + + if (step.done) { + // We've reached the end of the new children. We can delete the rest. + deleteRemainingChildren(returnFiber, oldFiber); + return resultingFirstChild; + } + + if (oldFiber === null) { + // If we don't have any more existing children we can choose a fast path + // since the rest will all be insertions. + for (; !step.done; newIdx++, step = newChildren.next()) { + const newFiber = createChild(returnFiber, step.value, expirationTime); + if (newFiber === null) { + continue; + } + lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); + if (previousNewFiber === null) { + // TODO: Move out of the loop. This only happens for the first run. + resultingFirstChild = newFiber; + } else { + previousNewFiber.sibling = newFiber; + } + previousNewFiber = newFiber; + } + return resultingFirstChild; + } + + // Add all children to a key map for quick lookups. + const existingChildren = mapRemainingChildren(returnFiber, oldFiber); + + // Keep scanning and use the map to restore deleted items as moves. + for (; !step.done; newIdx++, step = newChildren.next()) { + const newFiber = updateFromMap( + existingChildren, + returnFiber, + newIdx, + step.value, + expirationTime, + ); + if (newFiber !== null) { + if (shouldTrackSideEffects) { + if (newFiber.alternate !== null) { + // The new fiber is a work in progress, but if there exists a + // current, that means that we reused the fiber. We need to delete + // it from the child list so that we don't add it to the deletion + // list. + existingChildren.delete( + newFiber.key === null ? newIdx : newFiber.key, + ); + } + } + lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); + if (previousNewFiber === null) { + resultingFirstChild = newFiber; + } else { + previousNewFiber.sibling = newFiber; + } + previousNewFiber = newFiber; + } + } + + if (shouldTrackSideEffects) { + // Any existing children that weren't consumed above were deleted. We need + // to add them to the deletion list. + existingChildren.forEach(child => deleteChild(returnFiber, child)); + } + + return resultingFirstChild; + } + + function reconcileSingleTextNode( + returnFiber: Fiber, + currentFirstChild: Fiber | null, + textContent: string, + expirationTime: ExpirationTime, + ): Fiber { + // There's no need to check for keys on text nodes since we don't have a + // way to define them. + if (currentFirstChild !== null && currentFirstChild.tag === HostText) { + // We already have an existing node so let's just update it and delete + // the rest. + deleteRemainingChildren(returnFiber, currentFirstChild.sibling); + const existing = useFiber(currentFirstChild, textContent); + existing.return = returnFiber; + return existing; + } + // The existing first child is not a text node so we need to create one + // and delete the existing ones. + deleteRemainingChildren(returnFiber, currentFirstChild); + const created = createFiberFromText( + textContent, + returnFiber.mode, + expirationTime, + ); + created.return = returnFiber; + return created; + } + + function reconcileSingleElement( + returnFiber: Fiber, + currentFirstChild: Fiber | null, + element: ReactElement, + expirationTime: ExpirationTime, + ): Fiber { + const key = element.key; + let child = currentFirstChild; + while (child !== null) { + // TODO: If key === null and child.key === null, then this only applies to + // the first item in the list. + if (child.key === key) { + switch (child.tag) { + case Fragment: { + if (element.type === REACT_FRAGMENT_TYPE) { + deleteRemainingChildren(returnFiber, child.sibling); + const existing = useFiber(child, element.props.children); + existing.return = returnFiber; + if (__DEV__) { + existing._debugSource = element._source; + existing._debugOwner = element._owner; + } + return existing; + } + break; + } + case Block: + if (enableBlocksAPI) { + let type = element.type; + if (type.$$typeof === REACT_LAZY_TYPE) { + type = resolveLazyType(type); + } + if (type.$$typeof === REACT_BLOCK_TYPE) { + // The new Block might not be initialized yet. We need to initialize + // it in case initializing it turns out it would match. + if ( + ((type: any): BlockComponent)._render === + (child.type: BlockComponent)._render + ) { + deleteRemainingChildren(returnFiber, child.sibling); + const existing = useFiber(child, element.props); + existing.type = type; + existing.return = returnFiber; + if (__DEV__) { + existing._debugSource = element._source; + existing._debugOwner = element._owner; + } + return existing; + } + } + } + // We intentionally fallthrough here if enableBlocksAPI is not on. + // eslint-disable-next-lined no-fallthrough + default: { + if ( + child.elementType === element.type || + // Keep this check inline so it only runs on the false path: + (__DEV__ + ? isCompatibleFamilyForHotReloading(child, element) + : false) + ) { + deleteRemainingChildren(returnFiber, child.sibling); + const existing = useFiber(child, element.props); + existing.ref = coerceRef(returnFiber, child, element); + existing.return = returnFiber; + if (__DEV__) { + existing._debugSource = element._source; + existing._debugOwner = element._owner; + } + return existing; + } + break; + } + } + // Didn't match. + deleteRemainingChildren(returnFiber, child); + break; + } else { + deleteChild(returnFiber, child); + } + child = child.sibling; + } + + if (element.type === REACT_FRAGMENT_TYPE) { + const created = createFiberFromFragment( + element.props.children, + returnFiber.mode, + expirationTime, + element.key, + ); + created.return = returnFiber; + return created; + } else { + const created = createFiberFromElement( + element, + returnFiber.mode, + expirationTime, + ); + created.ref = coerceRef(returnFiber, currentFirstChild, element); + created.return = returnFiber; + return created; + } + } + + function reconcileSinglePortal( + returnFiber: Fiber, + currentFirstChild: Fiber | null, + portal: ReactPortal, + expirationTime: ExpirationTime, + ): Fiber { + const key = portal.key; + let child = currentFirstChild; + while (child !== null) { + // TODO: If key === null and child.key === null, then this only applies to + // the first item in the list. + if (child.key === key) { + if ( + child.tag === HostPortal && + child.stateNode.containerInfo === portal.containerInfo && + child.stateNode.implementation === portal.implementation + ) { + deleteRemainingChildren(returnFiber, child.sibling); + const existing = useFiber(child, portal.children || []); + existing.return = returnFiber; + return existing; + } else { + deleteRemainingChildren(returnFiber, child); + break; + } + } else { + deleteChild(returnFiber, child); + } + child = child.sibling; + } + + const created = createFiberFromPortal( + portal, + returnFiber.mode, + expirationTime, + ); + created.return = returnFiber; + return created; + } + + // This API will tag the children with the side-effect of the reconciliation + // itself. They will be added to the side-effect list as we pass through the + // children and the parent. + function reconcileChildFibers( + returnFiber: Fiber, + currentFirstChild: Fiber | null, + newChild: any, + expirationTime: ExpirationTime, + ): Fiber | null { + // This function is not recursive. + // If the top level item is an array, we treat it as a set of children, + // not as a fragment. Nested arrays on the other hand will be treated as + // fragment nodes. Recursion happens at the normal flow. + + // Handle top level unkeyed fragments as if they were arrays. + // This leads to an ambiguity between <>{[...]} and <>.... + // We treat the ambiguous cases above the same. + const isUnkeyedTopLevelFragment = + typeof newChild === 'object' && + newChild !== null && + newChild.type === REACT_FRAGMENT_TYPE && + newChild.key === null; + if (isUnkeyedTopLevelFragment) { + newChild = newChild.props.children; + } + + // Handle object types + const isObject = typeof newChild === 'object' && newChild !== null; + + if (isObject) { + switch (newChild.$$typeof) { + case REACT_ELEMENT_TYPE: + return placeSingleChild( + reconcileSingleElement( + returnFiber, + currentFirstChild, + newChild, + expirationTime, + ), + ); + case REACT_PORTAL_TYPE: + return placeSingleChild( + reconcileSinglePortal( + returnFiber, + currentFirstChild, + newChild, + expirationTime, + ), + ); + } + } + + if (typeof newChild === 'string' || typeof newChild === 'number') { + return placeSingleChild( + reconcileSingleTextNode( + returnFiber, + currentFirstChild, + '' + newChild, + expirationTime, + ), + ); + } + + if (isArray(newChild)) { + return reconcileChildrenArray( + returnFiber, + currentFirstChild, + newChild, + expirationTime, + ); + } + + if (getIteratorFn(newChild)) { + return reconcileChildrenIterator( + returnFiber, + currentFirstChild, + newChild, + expirationTime, + ); + } + + if (isObject) { + throwOnInvalidObjectType(returnFiber, newChild); + } + + if (__DEV__) { + if (typeof newChild === 'function') { + warnOnFunctionType(); + } + } + if (typeof newChild === 'undefined' && !isUnkeyedTopLevelFragment) { + // If the new child is undefined, and the return fiber is a composite + // component, throw an error. If Fiber return types are disabled, + // we already threw above. + switch (returnFiber.tag) { + case ClassComponent: { + if (__DEV__) { + const instance = returnFiber.stateNode; + if (instance.render._isMockFunction) { + // We allow auto-mocks to proceed as if they're returning null. + break; + } + } + } + // Intentionally fall through to the next case, which handles both + // functions and classes + // eslint-disable-next-lined no-fallthrough + case FunctionComponent: { + const Component = returnFiber.type; + invariant( + false, + '%s(...): Nothing was returned from render. This usually means a ' + + 'return statement is missing. Or, to render nothing, ' + + 'return null.', + Component.displayName || Component.name || 'Component', + ); + } + } + } + + // Remaining cases are all treated as empty. + return deleteRemainingChildren(returnFiber, currentFirstChild); + } + + return reconcileChildFibers; +} + +export const reconcileChildFibers = ChildReconciler(true); +export const mountChildFibers = ChildReconciler(false); + +export function cloneChildFibers( + current: Fiber | null, + workInProgress: Fiber, +): void { + invariant( + current === null || workInProgress.child === current.child, + 'Resuming work not yet implemented.', + ); + + if (workInProgress.child === null) { + return; + } + + let currentChild = workInProgress.child; + let newChild = createWorkInProgress(currentChild, currentChild.pendingProps); + workInProgress.child = newChild; + + newChild.return = workInProgress; + while (currentChild.sibling !== null) { + currentChild = currentChild.sibling; + newChild = newChild.sibling = createWorkInProgress( + currentChild, + currentChild.pendingProps, + ); + newChild.return = workInProgress; + } + newChild.sibling = null; +} + +// Reset a workInProgress child set to prepare it for a second pass. +export function resetChildFibers( + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +): void { + let child = workInProgress.child; + while (child !== null) { + resetWorkInProgress(child, renderExpirationTime); + child = child.sibling; + } +} diff --git a/packages/react-reconciler/src/ReactFiber.new.js b/packages/react-reconciler/src/ReactFiber.new.js new file mode 100644 index 0000000000000..97c8a7a87bb29 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiber.new.js @@ -0,0 +1,817 @@ +/** + * 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 {ReactElement} from 'shared/ReactElementType'; +import type { + ReactFragment, + ReactPortal, + ReactFundamentalComponent, + ReactScope, +} from 'shared/ReactTypes'; +import type {Fiber} from './ReactInternalTypes'; +import type {RootTag} from './ReactRootTags'; +import type {WorkTag} from './ReactWorkTags'; +import type {TypeOfMode} from './ReactTypeOfMode'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type {SuspenseInstance} from './ReactFiberHostConfig'; + +import invariant from 'shared/invariant'; +import { + enableProfilerTimer, + enableFundamentalAPI, + enableScopeAPI, + enableBlocksAPI, + throwEarlyForMysteriousError, +} from 'shared/ReactFeatureFlags'; +import {NoEffect, Placement} from './ReactSideEffectTags'; +import {ConcurrentRoot, BlockingRoot} from './ReactRootTags'; +import { + IndeterminateComponent, + ClassComponent, + HostRoot, + HostComponent, + HostText, + HostPortal, + ForwardRef, + Fragment, + Mode, + ContextProvider, + ContextConsumer, + Profiler, + SuspenseComponent, + SuspenseListComponent, + DehydratedFragment, + FunctionComponent, + MemoComponent, + SimpleMemoComponent, + LazyComponent, + FundamentalComponent, + ScopeComponent, + Block, +} from './ReactWorkTags'; +import getComponentName from 'shared/getComponentName'; + +import {isDevToolsPresent} from './ReactFiberDevToolsHook.new'; +import { + resolveClassForHotReloading, + resolveFunctionForHotReloading, + resolveForwardRefForHotReloading, +} from './ReactFiberHotReloading.new'; +import {NoWork} from './ReactFiberExpirationTime'; +import { + NoMode, + ConcurrentMode, + ProfileMode, + StrictMode, + BlockingMode, +} from './ReactTypeOfMode'; +import { + REACT_FORWARD_REF_TYPE, + REACT_FRAGMENT_TYPE, + REACT_STRICT_MODE_TYPE, + REACT_PROFILER_TYPE, + REACT_PROVIDER_TYPE, + REACT_CONTEXT_TYPE, + REACT_SUSPENSE_TYPE, + REACT_SUSPENSE_LIST_TYPE, + REACT_MEMO_TYPE, + REACT_LAZY_TYPE, + REACT_FUNDAMENTAL_TYPE, + REACT_SCOPE_TYPE, + REACT_BLOCK_TYPE, +} from 'shared/ReactSymbols'; + +export type {Fiber}; + +let hasBadMapPolyfill; + +if (__DEV__) { + hasBadMapPolyfill = false; + try { + const nonExtensibleObject = Object.preventExtensions({}); + /* eslint-disable no-new */ + new Map([[nonExtensibleObject, null]]); + new Set([nonExtensibleObject]); + /* eslint-enable no-new */ + } catch (e) { + // TODO: Consider warning about bad polyfills + hasBadMapPolyfill = true; + } +} + +let debugCounter = 1; + +function FiberNode( + tag: WorkTag, + pendingProps: mixed, + key: null | string, + mode: TypeOfMode, +) { + // Instance + this.tag = tag; + this.key = key; + this.elementType = null; + this.type = null; + this.stateNode = null; + + // Fiber + this.return = null; + this.child = null; + this.sibling = null; + this.index = 0; + + this.ref = null; + + this.pendingProps = pendingProps; + this.memoizedProps = null; + this.updateQueue = null; + this.memoizedState = null; + this.dependencies = null; + + this.mode = mode; + + // Effects + this.effectTag = NoEffect; + this.nextEffect = null; + + this.firstEffect = null; + this.lastEffect = null; + + this.expirationTime = NoWork; + this.childExpirationTime = NoWork; + + this.alternate = null; + + if (enableProfilerTimer) { + // Note: The following is done to avoid a v8 performance cliff. + // + // Initializing the fields below to smis and later updating them with + // double values will cause Fibers to end up having separate shapes. + // This behavior/bug has something to do with Object.preventExtension(). + // Fortunately this only impacts DEV builds. + // Unfortunately it makes React unusably slow for some applications. + // To work around this, initialize the fields below with doubles. + // + // Learn more about this here: + // https://github.com/facebook/react/issues/14365 + // https://bugs.chromium.org/p/v8/issues/detail?id=8538 + this.actualDuration = Number.NaN; + this.actualStartTime = Number.NaN; + this.selfBaseDuration = Number.NaN; + this.treeBaseDuration = Number.NaN; + + // It's okay to replace the initial doubles with smis after initialization. + // This won't trigger the performance cliff mentioned above, + // and it simplifies other profiler code (including DevTools). + this.actualDuration = 0; + this.actualStartTime = -1; + this.selfBaseDuration = 0; + this.treeBaseDuration = 0; + } + + if (__DEV__) { + // This isn't directly used but is handy for debugging internals: + this._debugID = debugCounter++; + this._debugSource = null; + this._debugOwner = null; + this._debugNeedsRemount = false; + this._debugHookTypes = null; + if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') { + Object.preventExtensions(this); + } + } +} + +// This is a constructor function, rather than a POJO constructor, still +// please ensure we do the following: +// 1) Nobody should add any instance methods on this. Instance methods can be +// more difficult to predict when they get optimized and they are almost +// never inlined properly in static compilers. +// 2) Nobody should rely on `instanceof Fiber` for type testing. We should +// always know when it is a fiber. +// 3) We might want to experiment with using numeric keys since they are easier +// to optimize in a non-JIT environment. +// 4) We can easily go from a constructor to a createFiber object literal if that +// is faster. +// 5) It should be easy to port this to a C struct and keep a C implementation +// compatible. +const createFiber = function( + tag: WorkTag, + pendingProps: mixed, + key: null | string, + mode: TypeOfMode, +): Fiber { + // $FlowFixMe: the shapes are exact here but Flow doesn't like constructors + return new FiberNode(tag, pendingProps, key, mode); +}; + +function shouldConstruct(Component: Function) { + const prototype = Component.prototype; + return !!(prototype && prototype.isReactComponent); +} + +export function isSimpleFunctionComponent(type: any) { + return ( + typeof type === 'function' && + !shouldConstruct(type) && + type.defaultProps === undefined + ); +} + +export function resolveLazyComponentTag(Component: Function): WorkTag { + if (typeof Component === 'function') { + return shouldConstruct(Component) ? ClassComponent : FunctionComponent; + } else if (Component !== undefined && Component !== null) { + const $$typeof = Component.$$typeof; + if ($$typeof === REACT_FORWARD_REF_TYPE) { + return ForwardRef; + } + if ($$typeof === REACT_MEMO_TYPE) { + return MemoComponent; + } + if (enableBlocksAPI) { + if ($$typeof === REACT_BLOCK_TYPE) { + return Block; + } + } + } + return IndeterminateComponent; +} + +// This is used to create an alternate fiber to do work on. +export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber { + let workInProgress = current.alternate; + if (workInProgress === null) { + // We use a double buffering pooling technique because we know that we'll + // only ever need at most two versions of a tree. We pool the "other" unused + // node that we're free to reuse. This is lazily created to avoid allocating + // extra objects for things that are never updated. It also allow us to + // reclaim the extra memory if needed. + workInProgress = createFiber( + current.tag, + pendingProps, + current.key, + current.mode, + ); + workInProgress.elementType = current.elementType; + workInProgress.type = current.type; + workInProgress.stateNode = current.stateNode; + + if (__DEV__) { + // DEV-only fields + workInProgress._debugID = current._debugID; + workInProgress._debugSource = current._debugSource; + workInProgress._debugOwner = current._debugOwner; + workInProgress._debugHookTypes = current._debugHookTypes; + } + + workInProgress.alternate = current; + current.alternate = workInProgress; + } else { + workInProgress.pendingProps = pendingProps; + + // We already have an alternate. + // Reset the effect tag. + workInProgress.effectTag = NoEffect; + + // The effect list is no longer valid. + workInProgress.nextEffect = null; + workInProgress.firstEffect = null; + workInProgress.lastEffect = null; + + if (enableProfilerTimer) { + // We intentionally reset, rather than copy, actualDuration & actualStartTime. + // This prevents time from endlessly accumulating in new commits. + // This has the downside of resetting values for different priority renders, + // But works for yielding (the common case) and should support resuming. + workInProgress.actualDuration = 0; + workInProgress.actualStartTime = -1; + } + } + + if (throwEarlyForMysteriousError) { + // Trying to debug a mysterious internal-only production failure. + // See D20130868 and t62461245. + // This is only on for RN FB builds. + if (current == null) { + throw Error('current is ' + current + " but it can't be"); + } + if (workInProgress == null) { + throw Error('workInProgress is ' + workInProgress + " but it can't be"); + } + } + + workInProgress.childExpirationTime = current.childExpirationTime; + workInProgress.expirationTime = current.expirationTime; + + workInProgress.child = current.child; + workInProgress.memoizedProps = current.memoizedProps; + workInProgress.memoizedState = current.memoizedState; + workInProgress.updateQueue = current.updateQueue; + + // Clone the dependencies object. This is mutated during the render phase, so + // it cannot be shared with the current fiber. + const currentDependencies = current.dependencies; + workInProgress.dependencies = + currentDependencies === null + ? null + : { + expirationTime: currentDependencies.expirationTime, + firstContext: currentDependencies.firstContext, + responders: currentDependencies.responders, + }; + + // These will be overridden during the parent's reconciliation + workInProgress.sibling = current.sibling; + workInProgress.index = current.index; + workInProgress.ref = current.ref; + + if (enableProfilerTimer) { + workInProgress.selfBaseDuration = current.selfBaseDuration; + workInProgress.treeBaseDuration = current.treeBaseDuration; + } + + if (__DEV__) { + workInProgress._debugNeedsRemount = current._debugNeedsRemount; + switch (workInProgress.tag) { + case IndeterminateComponent: + case FunctionComponent: + case SimpleMemoComponent: + workInProgress.type = resolveFunctionForHotReloading(current.type); + break; + case ClassComponent: + workInProgress.type = resolveClassForHotReloading(current.type); + break; + case ForwardRef: + workInProgress.type = resolveForwardRefForHotReloading(current.type); + break; + default: + break; + } + } + + return workInProgress; +} + +// Used to reuse a Fiber for a second pass. +export function resetWorkInProgress( + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +) { + // This resets the Fiber to what createFiber or createWorkInProgress would + // have set the values to before during the first pass. Ideally this wouldn't + // be necessary but unfortunately many code paths reads from the workInProgress + // when they should be reading from current and writing to workInProgress. + + // We assume pendingProps, index, key, ref, return are still untouched to + // avoid doing another reconciliation. + + // Reset the effect tag but keep any Placement tags, since that's something + // that child fiber is setting, not the reconciliation. + workInProgress.effectTag &= Placement; + + // The effect list is no longer valid. + workInProgress.nextEffect = null; + workInProgress.firstEffect = null; + workInProgress.lastEffect = null; + + const current = workInProgress.alternate; + if (current === null) { + // Reset to createFiber's initial values. + workInProgress.childExpirationTime = NoWork; + workInProgress.expirationTime = renderExpirationTime; + + workInProgress.child = null; + workInProgress.memoizedProps = null; + workInProgress.memoizedState = null; + workInProgress.updateQueue = null; + + workInProgress.dependencies = null; + + workInProgress.stateNode = null; + + if (enableProfilerTimer) { + // Note: We don't reset the actualTime counts. It's useful to accumulate + // actual time across multiple render passes. + workInProgress.selfBaseDuration = 0; + workInProgress.treeBaseDuration = 0; + } + } else { + // Reset to the cloned values that createWorkInProgress would've. + workInProgress.childExpirationTime = current.childExpirationTime; + workInProgress.expirationTime = current.expirationTime; + + workInProgress.child = current.child; + workInProgress.memoizedProps = current.memoizedProps; + workInProgress.memoizedState = current.memoizedState; + workInProgress.updateQueue = current.updateQueue; + + // Clone the dependencies object. This is mutated during the render phase, so + // it cannot be shared with the current fiber. + const currentDependencies = current.dependencies; + workInProgress.dependencies = + currentDependencies === null + ? null + : { + expirationTime: currentDependencies.expirationTime, + firstContext: currentDependencies.firstContext, + responders: currentDependencies.responders, + }; + + if (enableProfilerTimer) { + // Note: We don't reset the actualTime counts. It's useful to accumulate + // actual time across multiple render passes. + workInProgress.selfBaseDuration = current.selfBaseDuration; + workInProgress.treeBaseDuration = current.treeBaseDuration; + } + } + + return workInProgress; +} + +export function createHostRootFiber(tag: RootTag): Fiber { + let mode; + if (tag === ConcurrentRoot) { + mode = ConcurrentMode | BlockingMode | StrictMode; + } else if (tag === BlockingRoot) { + mode = BlockingMode | StrictMode; + } else { + mode = NoMode; + } + + if (enableProfilerTimer && isDevToolsPresent) { + // Always collect profile timings when DevTools are present. + // This enables DevTools to start capturing timing at any point– + // Without some nodes in the tree having empty base times. + mode |= ProfileMode; + } + + return createFiber(HostRoot, null, null, mode); +} + +export function createFiberFromTypeAndProps( + type: any, // React$ElementType + key: null | string, + pendingProps: any, + owner: null | Fiber, + mode: TypeOfMode, + expirationTime: ExpirationTime, +): Fiber { + let fiberTag = IndeterminateComponent; + // The resolved type is set if we know what the final type will be. I.e. it's not lazy. + let resolvedType = type; + if (typeof type === 'function') { + if (shouldConstruct(type)) { + fiberTag = ClassComponent; + if (__DEV__) { + resolvedType = resolveClassForHotReloading(resolvedType); + } + } else { + if (__DEV__) { + resolvedType = resolveFunctionForHotReloading(resolvedType); + } + } + } else if (typeof type === 'string') { + fiberTag = HostComponent; + } else { + getTag: switch (type) { + case REACT_FRAGMENT_TYPE: + return createFiberFromFragment( + pendingProps.children, + mode, + expirationTime, + key, + ); + case REACT_STRICT_MODE_TYPE: + fiberTag = Mode; + mode |= StrictMode; + break; + case REACT_PROFILER_TYPE: + return createFiberFromProfiler(pendingProps, mode, expirationTime, key); + case REACT_SUSPENSE_TYPE: + return createFiberFromSuspense(pendingProps, mode, expirationTime, key); + case REACT_SUSPENSE_LIST_TYPE: + return createFiberFromSuspenseList( + pendingProps, + mode, + expirationTime, + key, + ); + default: { + if (typeof type === 'object' && type !== null) { + switch (type.$$typeof) { + case REACT_PROVIDER_TYPE: + fiberTag = ContextProvider; + break getTag; + case REACT_CONTEXT_TYPE: + // This is a consumer + fiberTag = ContextConsumer; + break getTag; + case REACT_FORWARD_REF_TYPE: + fiberTag = ForwardRef; + if (__DEV__) { + resolvedType = resolveForwardRefForHotReloading(resolvedType); + } + break getTag; + case REACT_MEMO_TYPE: + fiberTag = MemoComponent; + break getTag; + case REACT_LAZY_TYPE: + fiberTag = LazyComponent; + resolvedType = null; + break getTag; + case REACT_BLOCK_TYPE: + fiberTag = Block; + break getTag; + case REACT_FUNDAMENTAL_TYPE: + if (enableFundamentalAPI) { + return createFiberFromFundamental( + type, + pendingProps, + mode, + expirationTime, + key, + ); + } + break; + case REACT_SCOPE_TYPE: + if (enableScopeAPI) { + return createFiberFromScope( + type, + pendingProps, + mode, + expirationTime, + key, + ); + } + } + } + let info = ''; + if (__DEV__) { + if ( + type === undefined || + (typeof type === 'object' && + type !== null && + Object.keys(type).length === 0) + ) { + info += + ' You likely forgot to export your component from the file ' + + "it's defined in, or you might have mixed up default and " + + 'named imports.'; + } + const ownerName = owner ? getComponentName(owner.type) : null; + if (ownerName) { + info += '\n\nCheck the render method of `' + ownerName + '`.'; + } + } + invariant( + false, + 'Element type is invalid: expected a string (for built-in ' + + 'components) or a class/function (for composite components) ' + + 'but got: %s.%s', + type == null ? type : typeof type, + info, + ); + } + } + } + + const fiber = createFiber(fiberTag, pendingProps, key, mode); + fiber.elementType = type; + fiber.type = resolvedType; + fiber.expirationTime = expirationTime; + + return fiber; +} + +export function createFiberFromElement( + element: ReactElement, + mode: TypeOfMode, + expirationTime: ExpirationTime, +): Fiber { + let owner = null; + if (__DEV__) { + owner = element._owner; + } + const type = element.type; + const key = element.key; + const pendingProps = element.props; + const fiber = createFiberFromTypeAndProps( + type, + key, + pendingProps, + owner, + mode, + expirationTime, + ); + if (__DEV__) { + fiber._debugSource = element._source; + fiber._debugOwner = element._owner; + } + return fiber; +} + +export function createFiberFromFragment( + elements: ReactFragment, + mode: TypeOfMode, + expirationTime: ExpirationTime, + key: null | string, +): Fiber { + const fiber = createFiber(Fragment, elements, key, mode); + fiber.expirationTime = expirationTime; + return fiber; +} + +export function createFiberFromFundamental( + fundamentalComponent: ReactFundamentalComponent, + pendingProps: any, + mode: TypeOfMode, + expirationTime: ExpirationTime, + key: null | string, +): Fiber { + const fiber = createFiber(FundamentalComponent, pendingProps, key, mode); + fiber.elementType = fundamentalComponent; + fiber.type = fundamentalComponent; + fiber.expirationTime = expirationTime; + return fiber; +} + +function createFiberFromScope( + scope: ReactScope, + pendingProps: any, + mode: TypeOfMode, + expirationTime: ExpirationTime, + key: null | string, +) { + const fiber = createFiber(ScopeComponent, pendingProps, key, mode); + fiber.type = scope; + fiber.elementType = scope; + fiber.expirationTime = expirationTime; + return fiber; +} + +function createFiberFromProfiler( + pendingProps: any, + mode: TypeOfMode, + expirationTime: ExpirationTime, + key: null | string, +): Fiber { + if (__DEV__) { + if (typeof pendingProps.id !== 'string') { + console.error('Profiler must specify an "id" as a prop'); + } + } + + const fiber = createFiber(Profiler, pendingProps, key, mode | ProfileMode); + // TODO: The Profiler fiber shouldn't have a type. It has a tag. + fiber.elementType = REACT_PROFILER_TYPE; + fiber.type = REACT_PROFILER_TYPE; + fiber.expirationTime = expirationTime; + + if (enableProfilerTimer) { + fiber.stateNode = { + effectDuration: 0, + passiveEffectDuration: 0, + }; + } + + return fiber; +} + +export function createFiberFromSuspense( + pendingProps: any, + mode: TypeOfMode, + expirationTime: ExpirationTime, + key: null | string, +) { + const fiber = createFiber(SuspenseComponent, pendingProps, key, mode); + + // TODO: The SuspenseComponent fiber shouldn't have a type. It has a tag. + // This needs to be fixed in getComponentName so that it relies on the tag + // instead. + fiber.type = REACT_SUSPENSE_TYPE; + fiber.elementType = REACT_SUSPENSE_TYPE; + + fiber.expirationTime = expirationTime; + return fiber; +} + +export function createFiberFromSuspenseList( + pendingProps: any, + mode: TypeOfMode, + expirationTime: ExpirationTime, + key: null | string, +) { + const fiber = createFiber(SuspenseListComponent, pendingProps, key, mode); + if (__DEV__) { + // TODO: The SuspenseListComponent fiber shouldn't have a type. It has a tag. + // This needs to be fixed in getComponentName so that it relies on the tag + // instead. + fiber.type = REACT_SUSPENSE_LIST_TYPE; + } + fiber.elementType = REACT_SUSPENSE_LIST_TYPE; + fiber.expirationTime = expirationTime; + return fiber; +} + +export function createFiberFromText( + content: string, + mode: TypeOfMode, + expirationTime: ExpirationTime, +): Fiber { + const fiber = createFiber(HostText, content, null, mode); + fiber.expirationTime = expirationTime; + return fiber; +} + +export function createFiberFromHostInstanceForDeletion(): Fiber { + const fiber = createFiber(HostComponent, null, null, NoMode); + // TODO: These should not need a type. + fiber.elementType = 'DELETED'; + fiber.type = 'DELETED'; + return fiber; +} + +export function createFiberFromDehydratedFragment( + dehydratedNode: SuspenseInstance, +): Fiber { + const fiber = createFiber(DehydratedFragment, null, null, NoMode); + fiber.stateNode = dehydratedNode; + return fiber; +} + +export function createFiberFromPortal( + portal: ReactPortal, + mode: TypeOfMode, + expirationTime: ExpirationTime, +): Fiber { + const pendingProps = portal.children !== null ? portal.children : []; + const fiber = createFiber(HostPortal, pendingProps, portal.key, mode); + fiber.expirationTime = expirationTime; + fiber.stateNode = { + containerInfo: portal.containerInfo, + pendingChildren: null, // Used by persistent updates + implementation: portal.implementation, + }; + return fiber; +} + +// Used for stashing WIP properties to replay failed work in DEV. +export function assignFiberPropertiesInDEV( + target: Fiber | null, + source: Fiber, +): Fiber { + if (target === null) { + // This Fiber's initial properties will always be overwritten. + // We only use a Fiber to ensure the same hidden class so DEV isn't slow. + target = createFiber(IndeterminateComponent, null, null, NoMode); + } + + // This is intentionally written as a list of all properties. + // We tried to use Object.assign() instead but this is called in + // the hottest path, and Object.assign() was too slow: + // https://github.com/facebook/react/issues/12502 + // This code is DEV-only so size is not a concern. + + target.tag = source.tag; + target.key = source.key; + target.elementType = source.elementType; + target.type = source.type; + target.stateNode = source.stateNode; + target.return = source.return; + target.child = source.child; + target.sibling = source.sibling; + target.index = source.index; + target.ref = source.ref; + target.pendingProps = source.pendingProps; + target.memoizedProps = source.memoizedProps; + target.updateQueue = source.updateQueue; + target.memoizedState = source.memoizedState; + target.dependencies = source.dependencies; + target.mode = source.mode; + target.effectTag = source.effectTag; + target.nextEffect = source.nextEffect; + target.firstEffect = source.firstEffect; + target.lastEffect = source.lastEffect; + target.expirationTime = source.expirationTime; + target.childExpirationTime = source.childExpirationTime; + target.alternate = source.alternate; + if (enableProfilerTimer) { + target.actualDuration = source.actualDuration; + target.actualStartTime = source.actualStartTime; + target.selfBaseDuration = source.selfBaseDuration; + target.treeBaseDuration = source.treeBaseDuration; + } + target._debugID = source._debugID; + target._debugSource = source._debugSource; + target._debugOwner = source._debugOwner; + target._debugNeedsRemount = source._debugNeedsRemount; + target._debugHookTypes = source._debugHookTypes; + return target; +} diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js new file mode 100644 index 0000000000000..513599cdc57a0 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -0,0 +1,3494 @@ +/** + * 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 {BlockComponent} from 'react/src/ReactBlock'; +import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy'; +import type {Fiber} from './ReactInternalTypes'; +import type {FiberRoot} from './ReactInternalTypes'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type { + SuspenseState, + SuspenseListRenderState, + SuspenseListTailMode, +} from './ReactFiberSuspenseComponent.new'; +import type {SuspenseContext} from './ReactFiberSuspenseContext.new'; + +import checkPropTypes from 'shared/checkPropTypes'; + +import { + IndeterminateComponent, + FunctionComponent, + ClassComponent, + HostRoot, + HostComponent, + HostText, + HostPortal, + ForwardRef, + Fragment, + Mode, + ContextProvider, + ContextConsumer, + Profiler, + SuspenseComponent, + SuspenseListComponent, + MemoComponent, + SimpleMemoComponent, + LazyComponent, + IncompleteClassComponent, + FundamentalComponent, + ScopeComponent, + Block, +} from './ReactWorkTags'; +import { + NoEffect, + PerformedWork, + Placement, + Hydrating, + ContentReset, + DidCapture, + Update, + Ref, + Deletion, +} from './ReactSideEffectTags'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; +import { + debugRenderPhaseSideEffectsForStrictMode, + disableLegacyContext, + disableModulePatternComponents, + enableProfilerTimer, + enableSchedulerTracing, + enableSuspenseServerRenderer, + enableFundamentalAPI, + warnAboutDefaultPropsOnFunctionComponents, + enableScopeAPI, + enableBlocksAPI, +} from 'shared/ReactFeatureFlags'; +import invariant from 'shared/invariant'; +import shallowEqual from 'shared/shallowEqual'; +import getComponentName from 'shared/getComponentName'; +import ReactStrictModeWarnings from './ReactStrictModeWarnings.new'; +import {REACT_LAZY_TYPE, getIteratorFn} from 'shared/ReactSymbols'; +import { + getCurrentFiberOwnerNameInDevOrNull, + setIsRendering, +} from './ReactCurrentFiber'; +import { + resolveFunctionForHotReloading, + resolveForwardRefForHotReloading, + resolveClassForHotReloading, +} from './ReactFiberHotReloading.new'; + +import { + mountChildFibers, + reconcileChildFibers, + cloneChildFibers, +} from './ReactChildFiber.new'; +import { + processUpdateQueue, + cloneUpdateQueue, + initializeUpdateQueue, +} from './ReactUpdateQueue.new'; +import { + NoWork, + Never, + Sync, + computeAsyncExpiration, +} from './ReactFiberExpirationTime'; +import { + ConcurrentMode, + NoMode, + ProfileMode, + StrictMode, + BlockingMode, +} from './ReactTypeOfMode'; +import { + shouldSetTextContent, + shouldDeprioritizeSubtree, + isSuspenseInstancePending, + isSuspenseInstanceFallback, + registerSuspenseInstanceRetry, +} from './ReactFiberHostConfig'; +import type {SuspenseInstance} from './ReactFiberHostConfig'; +import {shouldSuspend} from './ReactFiberReconciler'; +import {pushHostContext, pushHostContainer} from './ReactFiberHostContext.new'; +import { + suspenseStackCursor, + pushSuspenseContext, + InvisibleParentSuspenseContext, + ForceSuspenseFallback, + hasSuspenseContext, + setDefaultShallowSuspenseContext, + addSubtreeSuspenseContext, + setShallowSuspenseContext, +} from './ReactFiberSuspenseContext.new'; +import {findFirstSuspended} from './ReactFiberSuspenseComponent.new'; +import { + pushProvider, + propagateContextChange, + readContext, + prepareToReadContext, + calculateChangedBits, + scheduleWorkOnParentPath, +} from './ReactFiberNewContext.new'; +import {renderWithHooks, bailoutHooks} from './ReactFiberHooks.new'; +import {stopProfilerTimerIfRunning} from './ReactProfilerTimer.new'; +import { + getMaskedContext, + getUnmaskedContext, + hasContextChanged as hasLegacyContextChanged, + pushContextProvider as pushLegacyContextProvider, + isContextProvider as isLegacyContextProvider, + pushTopLevelContextObject, + invalidateContextProvider, +} from './ReactFiberContext.new'; +import { + enterHydrationState, + reenterHydrationStateFromDehydratedSuspenseInstance, + resetHydrationState, + tryToClaimNextHydratableInstance, + warnIfHydrating, +} from './ReactFiberHydrationContext.new'; +import { + adoptClassInstance, + applyDerivedStateFromProps, + constructClassInstance, + mountClassInstance, + resumeMountClassInstance, + updateClassInstance, +} from './ReactFiberClassComponent.new'; +import {resolveDefaultProps} from './ReactFiberLazyComponent.new'; +import { + resolveLazyComponentTag, + createFiberFromTypeAndProps, + createFiberFromFragment, + createWorkInProgress, + isSimpleFunctionComponent, +} from './ReactFiber.new'; +import { + markSpawnedWork, + requestCurrentTimeForUpdate, + retryDehydratedSuspenseBoundary, + scheduleUpdateOnFiber, + renderDidSuspendDelayIfPossible, + markUnprocessedUpdateTime, + getWorkInProgressRoot, +} from './ReactFiberWorkLoop.new'; + +import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev'; + +const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; + +let didReceiveUpdate: boolean = false; + +let didWarnAboutBadClass; +let didWarnAboutModulePatternComponent; +let didWarnAboutContextTypeOnFunctionComponent; +let didWarnAboutGetDerivedStateOnFunctionComponent; +let didWarnAboutFunctionRefs; +export let didWarnAboutReassigningProps; +let didWarnAboutRevealOrder; +let didWarnAboutTailOptions; +let didWarnAboutDefaultPropsOnFunctionComponent; + +if (__DEV__) { + didWarnAboutBadClass = {}; + didWarnAboutModulePatternComponent = {}; + didWarnAboutContextTypeOnFunctionComponent = {}; + didWarnAboutGetDerivedStateOnFunctionComponent = {}; + didWarnAboutFunctionRefs = {}; + didWarnAboutReassigningProps = false; + didWarnAboutRevealOrder = {}; + didWarnAboutTailOptions = {}; + didWarnAboutDefaultPropsOnFunctionComponent = {}; +} + +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 + // identities match. + workInProgress.child = reconcileChildFibers( + workInProgress, + null, + nextChildren, + renderExpirationTime, + ); +} + +function updateForwardRef( + current: Fiber | null, + workInProgress: Fiber, + Component: any, + nextProps: any, + renderExpirationTime: ExpirationTime, +) { + // TODO: current can be non-null here even if the component + // hasn't yet mounted. This happens after the first render suspends. + // We'll need to figure out if this is fine or can cause issues. + + if (__DEV__) { + if (workInProgress.type !== workInProgress.elementType) { + // Lazy component props can't be validated in createElement + // because they're only guaranteed to be resolved here. + const innerPropTypes = Component.propTypes; + if (innerPropTypes) { + checkPropTypes( + innerPropTypes, + nextProps, // Resolved props + 'prop', + getComponentName(Component), + ); + } + } + } + + const render = Component.render; + const ref = workInProgress.ref; + + // The rest is a fork of updateFunctionComponent + let nextChildren; + prepareToReadContext(workInProgress, renderExpirationTime); + if (__DEV__) { + ReactCurrentOwner.current = workInProgress; + setIsRendering(true); + nextChildren = renderWithHooks( + current, + workInProgress, + render, + nextProps, + ref, + renderExpirationTime, + ); + if ( + debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode + ) { + disableLogs(); + try { + nextChildren = renderWithHooks( + current, + workInProgress, + render, + nextProps, + ref, + renderExpirationTime, + ); + } finally { + reenableLogs(); + } + } + setIsRendering(false); + } else { + nextChildren = renderWithHooks( + current, + workInProgress, + render, + nextProps, + ref, + renderExpirationTime, + ); + } + + if (current !== null && !didReceiveUpdate) { + bailoutHooks(current, workInProgress, renderExpirationTime); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); + } + + // React DevTools reads this flag. + workInProgress.effectTag |= PerformedWork; + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); + return workInProgress.child; +} + +function updateMemoComponent( + current: Fiber | null, + workInProgress: Fiber, + Component: any, + nextProps: any, + updateExpirationTime, + renderExpirationTime: ExpirationTime, +): null | Fiber { + if (current === null) { + const type = Component.type; + if ( + isSimpleFunctionComponent(type) && + Component.compare === null && + // SimpleMemoComponent codepath doesn't resolve outer props either. + Component.defaultProps === undefined + ) { + let resolvedType = type; + if (__DEV__) { + resolvedType = resolveFunctionForHotReloading(type); + } + // If this is a plain function component without default props, + // and with only the default shallow comparison, we upgrade it + // to a SimpleMemoComponent to allow fast path updates. + workInProgress.tag = SimpleMemoComponent; + workInProgress.type = resolvedType; + if (__DEV__) { + validateFunctionComponentInDev(workInProgress, type); + } + return updateSimpleMemoComponent( + current, + workInProgress, + resolvedType, + nextProps, + updateExpirationTime, + renderExpirationTime, + ); + } + if (__DEV__) { + const innerPropTypes = type.propTypes; + if (innerPropTypes) { + // Inner memo component props aren't currently validated in createElement. + // We could move it there, but we'd still need this for lazy code path. + checkPropTypes( + innerPropTypes, + nextProps, // Resolved props + 'prop', + getComponentName(type), + ); + } + } + const child = createFiberFromTypeAndProps( + Component.type, + null, + nextProps, + null, + workInProgress.mode, + renderExpirationTime, + ); + child.ref = workInProgress.ref; + child.return = workInProgress; + workInProgress.child = child; + return child; + } + if (__DEV__) { + const type = Component.type; + const innerPropTypes = type.propTypes; + if (innerPropTypes) { + // Inner memo component props aren't currently validated in createElement. + // We could move it there, but we'd still need this for lazy code path. + checkPropTypes( + innerPropTypes, + nextProps, // Resolved props + 'prop', + getComponentName(type), + ); + } + } + const currentChild = ((current.child: any): Fiber); // This is always exactly one child + if (updateExpirationTime < renderExpirationTime) { + // This will be the props with resolved defaultProps, + // unlike current.memoizedProps which will be the unresolved ones. + const prevProps = currentChild.memoizedProps; + // Default to shallow comparison + let compare = Component.compare; + compare = compare !== null ? compare : shallowEqual; + if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) { + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); + } + } + // React DevTools reads this flag. + workInProgress.effectTag |= PerformedWork; + const newChild = createWorkInProgress(currentChild, nextProps); + newChild.ref = workInProgress.ref; + newChild.return = workInProgress; + workInProgress.child = newChild; + return newChild; +} + +function updateSimpleMemoComponent( + current: Fiber | null, + workInProgress: Fiber, + Component: any, + nextProps: any, + updateExpirationTime, + renderExpirationTime: ExpirationTime, +): null | Fiber { + // TODO: current can be non-null here even if the component + // hasn't yet mounted. This happens when the inner render suspends. + // We'll need to figure out if this is fine or can cause issues. + + if (__DEV__) { + if (workInProgress.type !== workInProgress.elementType) { + // Lazy component props can't be validated in createElement + // because they're only guaranteed to be resolved here. + let outerMemoType = workInProgress.elementType; + if (outerMemoType.$$typeof === REACT_LAZY_TYPE) { + // We warn when you define propTypes on lazy() + // so let's just skip over it to find memo() outer wrapper. + // Inner props for memo are validated later. + const lazyComponent: LazyComponentType = outerMemoType; + const payload = lazyComponent._payload; + const init = lazyComponent._init; + try { + outerMemoType = init(payload); + } catch (x) { + outerMemoType = null; + } + // Inner propTypes will be validated in the function component path. + const outerPropTypes = outerMemoType && (outerMemoType: any).propTypes; + if (outerPropTypes) { + checkPropTypes( + outerPropTypes, + nextProps, // Resolved (SimpleMemoComponent has no defaultProps) + 'prop', + getComponentName(outerMemoType), + ); + } + } + } + } + if (current !== null) { + const prevProps = current.memoizedProps; + if ( + shallowEqual(prevProps, nextProps) && + current.ref === workInProgress.ref && + // Prevent bailout if the implementation changed due to hot reload. + (__DEV__ ? workInProgress.type === current.type : true) + ) { + didReceiveUpdate = false; + if (updateExpirationTime < renderExpirationTime) { + // The pending update priority was cleared at the beginning of + // beginWork. We're about to bail out, but there might be additional + // updates at a lower priority. Usually, the priority level of the + // remaining updates is accumlated during the evaluation of the + // component (i.e. when processing the update queue). But since since + // we're bailing out early *without* evaluating the component, we need + // to account for it here, too. Reset to the value of the current fiber. + // NOTE: This only applies to SimpleMemoComponent, not MemoComponent, + // because a MemoComponent fiber does not have hooks or an update queue; + // rather, it wraps around an inner component, which may or may not + // contains hooks. + // TODO: Move the reset at in beginWork out of the common path so that + // this is no longer necessary. + workInProgress.expirationTime = current.expirationTime; + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); + } + } + } + return updateFunctionComponent( + current, + workInProgress, + Component, + nextProps, + renderExpirationTime, + ); +} + +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; + + // Reset effect durations for the next eventual effect phase. + // These are reset during render to allow the DevTools commit hook a chance to read them, + const stateNode = workInProgress.stateNode; + stateNode.effectDuration = 0; + stateNode.passiveEffectDuration = 0; + } + 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, +) { + if (__DEV__) { + if (workInProgress.type !== workInProgress.elementType) { + // Lazy component props can't be validated in createElement + // because they're only guaranteed to be resolved here. + const innerPropTypes = Component.propTypes; + if (innerPropTypes) { + checkPropTypes( + innerPropTypes, + nextProps, // Resolved props + 'prop', + getComponentName(Component), + ); + } + } + } + + let context; + if (!disableLegacyContext) { + const unmaskedContext = getUnmaskedContext(workInProgress, Component, true); + context = getMaskedContext(workInProgress, unmaskedContext); + } + + let nextChildren; + prepareToReadContext(workInProgress, renderExpirationTime); + if (__DEV__) { + ReactCurrentOwner.current = workInProgress; + setIsRendering(true); + nextChildren = renderWithHooks( + current, + workInProgress, + Component, + nextProps, + context, + renderExpirationTime, + ); + if ( + debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode + ) { + disableLogs(); + try { + nextChildren = renderWithHooks( + current, + workInProgress, + Component, + nextProps, + context, + renderExpirationTime, + ); + } finally { + reenableLogs(); + } + } + setIsRendering(false); + } else { + nextChildren = renderWithHooks( + current, + workInProgress, + Component, + nextProps, + context, + renderExpirationTime, + ); + } + + if (current !== null && !didReceiveUpdate) { + bailoutHooks(current, workInProgress, renderExpirationTime); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); + } + + // React DevTools reads this flag. + workInProgress.effectTag |= PerformedWork; + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); + return workInProgress.child; +} + +function updateBlock( + current: Fiber | null, + workInProgress: Fiber, + block: BlockComponent, + nextProps: any, + renderExpirationTime: ExpirationTime, +) { + // TODO: current can be non-null here even if the component + // hasn't yet mounted. This happens after the first render suspends. + // We'll need to figure out if this is fine or can cause issues. + + const render = block._render; + const data = block._data; + + // The rest is a fork of updateFunctionComponent + let nextChildren; + prepareToReadContext(workInProgress, renderExpirationTime); + if (__DEV__) { + ReactCurrentOwner.current = workInProgress; + setIsRendering(true); + nextChildren = renderWithHooks( + current, + workInProgress, + render, + nextProps, + data, + renderExpirationTime, + ); + if ( + debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode + ) { + disableLogs(); + try { + nextChildren = renderWithHooks( + current, + workInProgress, + render, + nextProps, + data, + renderExpirationTime, + ); + } finally { + reenableLogs(); + } + } + setIsRendering(false); + } else { + nextChildren = renderWithHooks( + current, + workInProgress, + render, + nextProps, + data, + renderExpirationTime, + ); + } + + if (current !== null && !didReceiveUpdate) { + bailoutHooks(current, workInProgress, renderExpirationTime); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); + } + + // 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, +) { + if (__DEV__) { + if (workInProgress.type !== workInProgress.elementType) { + // Lazy component props can't be validated in createElement + // because they're only guaranteed to be resolved here. + const innerPropTypes = Component.propTypes; + if (innerPropTypes) { + checkPropTypes( + innerPropTypes, + nextProps, // Resolved props + 'prop', + getComponentName(Component), + ); + } + } + } + + // 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) { + // A class component without an instance only mounts if it suspended + // inside a non-concurrent tree, in an inconsistent state. We want to + // treat 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); + 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, + ); + } + const nextUnitOfWork = finishClassComponent( + current, + workInProgress, + Component, + shouldUpdate, + hasContext, + renderExpirationTime, + ); + if (__DEV__) { + const inst = workInProgress.stateNode; + if (shouldUpdate && inst.props !== nextProps) { + if (!didWarnAboutReassigningProps) { + console.error( + 'It looks like %s is reassigning its own `this.props` while rendering. ' + + 'This is not supported and can lead to confusing bugs.', + getComponentName(workInProgress.type) || 'a component', + ); + } + didWarnAboutReassigningProps = true; + } + } + return nextUnitOfWork; +} + +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 getDerivedStateFromError 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__) { + setIsRendering(true); + nextChildren = instance.render(); + if ( + debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode + ) { + disableLogs(); + try { + instance.render(); + } finally { + reenableLogs(); + } + } + setIsRendering(false); + } 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( + current !== null && 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; + cloneUpdateQueue(current, workInProgress); + processUpdateQueue(workInProgress, 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 (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. + + const child = mountChildFibers( + workInProgress, + null, + nextChildren, + renderExpirationTime, + ); + workInProgress.child = child; + + let node = child; + while (node) { + // Mark each child as hydrating. This is a fast path to know whether this + // tree is part of a hydrating tree. This is used to determine if a child + // node has fully mounted yet, and for scheduling event replaying. + // Conceptually this is similar to Placement in that a new subtree is + // inserted into the React tree here. It just happens to not need DOM + // mutations because it already exists. + node.effectTag = (node.effectTag & ~Placement) | Hydrating; + node = node.sibling; + } + } 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 has 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 ( + workInProgress.mode & ConcurrentMode && + renderExpirationTime !== Never && + shouldDeprioritizeSubtree(type, nextProps) + ) { + if (enableSchedulerTracing) { + markSpawnedWork(Never); + } + // Schedule this fiber to re-render at offscreen priority. Then bailout. + workInProgress.expirationTime = workInProgress.childExpirationTime = 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 mountLazyComponent( + _current, + workInProgress, + elementType, + updateExpirationTime, + renderExpirationTime, +) { + if (_current !== null) { + // A lazy component only mounts if it suspended inside a non- + // concurrent tree, in an inconsistent state. We want to treat 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; + const lazyComponent: LazyComponentType = elementType; + const payload = lazyComponent._payload; + const init = lazyComponent._init; + let Component = init(payload); + // Store the unwrapped component in the type. + workInProgress.type = Component; + const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component)); + const resolvedProps = resolveDefaultProps(Component, props); + let child; + switch (resolvedTag) { + case FunctionComponent: { + if (__DEV__) { + validateFunctionComponentInDev(workInProgress, Component); + workInProgress.type = Component = resolveFunctionForHotReloading( + Component, + ); + } + child = updateFunctionComponent( + null, + workInProgress, + Component, + resolvedProps, + renderExpirationTime, + ); + return child; + } + case ClassComponent: { + if (__DEV__) { + workInProgress.type = Component = resolveClassForHotReloading( + Component, + ); + } + child = updateClassComponent( + null, + workInProgress, + Component, + resolvedProps, + renderExpirationTime, + ); + return child; + } + case ForwardRef: { + if (__DEV__) { + workInProgress.type = Component = resolveForwardRefForHotReloading( + Component, + ); + } + child = updateForwardRef( + null, + workInProgress, + Component, + resolvedProps, + renderExpirationTime, + ); + return child; + } + case MemoComponent: { + if (__DEV__) { + if (workInProgress.type !== workInProgress.elementType) { + const outerPropTypes = Component.propTypes; + if (outerPropTypes) { + checkPropTypes( + outerPropTypes, + resolvedProps, // Resolved for outer only + 'prop', + getComponentName(Component), + ); + } + } + } + child = updateMemoComponent( + null, + workInProgress, + Component, + resolveDefaultProps(Component.type, resolvedProps), // The inner type can have defaults too + updateExpirationTime, + renderExpirationTime, + ); + return child; + } + case Block: { + if (enableBlocksAPI) { + // TODO: Resolve for Hot Reloading. + child = updateBlock( + null, + workInProgress, + Component, + props, + renderExpirationTime, + ); + return child; + } + break; + } + } + let hint = ''; + if (__DEV__) { + if ( + Component !== null && + typeof Component === 'object' && + Component.$$typeof === REACT_LAZY_TYPE + ) { + hint = ' Did you wrap a component in React.lazy() more than once?'; + } + } + // This message intentionally doesn't mention ForwardRef or MemoComponent + // 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. ' + + 'Lazy element type must resolve to a class or function.%s', + Component, + hint, + ); +} + +function mountIncompleteClassComponent( + _current, + workInProgress, + Component, + nextProps, + renderExpirationTime, +) { + if (_current !== null) { + // An incomplete component only mounts if it suspended inside a non- + // concurrent tree, in an inconsistent state. We want to treat 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; + } + + // Promote the fiber to a class and try rendering again. + workInProgress.tag = ClassComponent; + + // The rest of this function is a fork of `updateClassComponent` + + // 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); + + constructClassInstance(workInProgress, Component, nextProps); + mountClassInstance( + workInProgress, + Component, + nextProps, + renderExpirationTime, + ); + + return finishClassComponent( + null, + workInProgress, + Component, + true, + hasContext, + renderExpirationTime, + ); +} + +function mountIndeterminateComponent( + _current, + workInProgress, + Component, + renderExpirationTime, +) { + if (_current !== null) { + // An indeterminate component only mounts if it suspended inside a non- + // concurrent tree, in an inconsistent state. We want to treat 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; + let context; + if (!disableLegacyContext) { + const unmaskedContext = getUnmaskedContext( + workInProgress, + Component, + false, + ); + 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]) { + console.error( + "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); + } + + setIsRendering(true); + ReactCurrentOwner.current = workInProgress; + value = renderWithHooks( + null, + workInProgress, + Component, + props, + context, + renderExpirationTime, + ); + setIsRendering(false); + } else { + value = renderWithHooks( + null, + workInProgress, + Component, + props, + context, + renderExpirationTime, + ); + } + // React DevTools reads this flag. + workInProgress.effectTag |= PerformedWork; + + if (__DEV__) { + // Support for module components is deprecated and is removed behind a flag. + // Whether or not it would crash later, we want to show a good message in DEV first. + if ( + typeof value === 'object' && + value !== null && + typeof value.render === 'function' && + value.$$typeof === undefined + ) { + const componentName = getComponentName(Component) || 'Unknown'; + if (!didWarnAboutModulePatternComponent[componentName]) { + console.error( + 'The <%s /> component appears to be a function component that returns a class instance. ' + + 'Change %s to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " + + 'cannot be called with `new` by React.', + componentName, + componentName, + componentName, + ); + didWarnAboutModulePatternComponent[componentName] = true; + } + } + } + + if ( + // Run these checks in production only if the flag is off. + // Eventually we'll delete this branch altogether. + !disableModulePatternComponents && + typeof value === 'object' && + value !== null && + typeof value.render === 'function' && + value.$$typeof === undefined + ) { + if (__DEV__) { + const componentName = getComponentName(Component) || 'Unknown'; + if (!didWarnAboutModulePatternComponent[componentName]) { + console.error( + 'The <%s /> component appears to be a function component that returns a class instance. ' + + 'Change %s to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " + + 'cannot be called with `new` by React.', + componentName, + componentName, + componentName, + ); + didWarnAboutModulePatternComponent[componentName] = true; + } + } + + // Proceed under the assumption that this is a class instance + workInProgress.tag = ClassComponent; + + // Throw out any hooks that were used. + workInProgress.memoizedState = null; + workInProgress.updateQueue = null; + + // 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; + + initializeUpdateQueue(workInProgress); + + 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 (disableLegacyContext && Component.contextTypes) { + console.error( + '%s uses the legacy contextTypes API which is no longer supported. ' + + 'Use React.createContext() with React.useContext() instead.', + getComponentName(Component) || 'Unknown', + ); + } + + if ( + debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode + ) { + disableLogs(); + try { + value = renderWithHooks( + null, + workInProgress, + Component, + props, + context, + renderExpirationTime, + ); + } finally { + reenableLogs(); + } + } + } + reconcileChildren(null, workInProgress, value, renderExpirationTime); + if (__DEV__) { + validateFunctionComponentInDev(workInProgress, Component); + } + return workInProgress.child; + } +} + +function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) { + if (__DEV__) { + if (Component) { + if (Component.childContextTypes) { + console.error( + '%s(...): childContextTypes cannot be defined on a function component.', + Component.displayName || Component.name || 'Component', + ); + } + } + if (workInProgress.ref !== null) { + let info = ''; + const ownerName = 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; + console.error( + 'Function components cannot be given refs. ' + + 'Attempts to access this ref will fail. ' + + 'Did you mean to use React.forwardRef()?%s', + info, + ); + } + } + + if ( + warnAboutDefaultPropsOnFunctionComponents && + Component.defaultProps !== undefined + ) { + const componentName = getComponentName(Component) || 'Unknown'; + + if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) { + console.error( + '%s: Support for defaultProps will be removed from function components ' + + 'in a future major release. Use JavaScript default parameters instead.', + componentName, + ); + didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true; + } + } + + if (typeof Component.getDerivedStateFromProps === 'function') { + const componentName = getComponentName(Component) || 'Unknown'; + + if (!didWarnAboutGetDerivedStateOnFunctionComponent[componentName]) { + console.error( + '%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]) { + console.error( + '%s: Function components do not support contextType.', + componentName, + ); + didWarnAboutContextTypeOnFunctionComponent[componentName] = true; + } + } + } +} + +function mountSuspenseState( + renderExpirationTime: ExpirationTime, +): SuspenseState { + return { + dehydrated: null, + baseTime: renderExpirationTime, + retryTime: NoWork, + }; +} + +function updateSuspenseState( + prevSuspenseState: SuspenseState, + renderExpirationTime: ExpirationTime, +): SuspenseState { + const prevSuspendedTime = prevSuspenseState.baseTime; + return { + dehydrated: null, + baseTime: + // Choose whichever time is inclusive of the other one. This represents + // the union of all the levels that suspended. + prevSuspendedTime !== NoWork && prevSuspendedTime < renderExpirationTime + ? prevSuspendedTime + : renderExpirationTime, + retryTime: NoWork, + }; +} + +function shouldRemainOnFallback( + suspenseContext: SuspenseContext, + current: null | Fiber, + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +) { + // If we're already showing a fallback, there are cases where we need to + // remain on that fallback regardless of whether the content has resolved. + // For example, SuspenseList coordinates when nested content appears. + if (current !== null) { + const suspenseState: SuspenseState = current.memoizedState; + if (suspenseState !== null) { + // Currently showing a fallback. If the current render includes + // the level that triggered the fallback, we must continue showing it, + // regardless of what the Suspense context says. + const baseTime = suspenseState.baseTime; + if (baseTime !== NoWork && baseTime < renderExpirationTime) { + return true; + } + // Otherwise, fall through to check the Suspense context. + } else { + // Currently showing content. Don't hide it, even if ForceSuspenseFallack + // is true. More precise name might be "ForceRemainSuspenseFallback". + // Note: This is a factoring smell. Can't remain on a fallback if there's + // no fallback to remain on. + return false; + } + } + // Not currently showing content. Consult the Suspense context. + return hasSuspenseContext( + suspenseContext, + (ForceSuspenseFallback: SuspenseContext), + ); +} + +function getRemainingWorkInPrimaryTree( + current: Fiber, + workInProgress: Fiber, + renderExpirationTime, +) { + const currentChildExpirationTime = current.childExpirationTime; + const currentSuspenseState: SuspenseState = current.memoizedState; + if (currentSuspenseState !== null) { + // This boundary already timed out. Check if this render includes the level + // that previously suspended. + const baseTime = currentSuspenseState.baseTime; + if ( + baseTime !== NoWork && + baseTime < renderExpirationTime && + baseTime > currentChildExpirationTime + ) { + // There's pending work at a lower level that might now be unblocked. + return baseTime; + } + } + + if (currentChildExpirationTime < renderExpirationTime) { + // The highest priority remaining work is not part of this render. So the + // remaining work has not changed. + return currentChildExpirationTime; + } + + if ((workInProgress.mode & BlockingMode) !== NoMode) { + // The highest priority remaining work is part of this render. Since we only + // keep track of the highest level, we don't know if there's a lower + // priority level scheduled. As a compromise, we'll render at the lowest + // known level in the entire tree, since that will include everything. + // TODO: If expirationTime were a bitmask where each bit represents a + // separate task thread, this would be: currentChildBits & ~renderBits + const root = getWorkInProgressRoot(); + if (root !== null) { + const lastPendingTime = root.lastPendingTime; + if (lastPendingTime < renderExpirationTime) { + return lastPendingTime; + } + } + } + // In legacy mode, there's no work left. + return NoWork; +} + +function updateSuspenseComponent( + current, + workInProgress, + renderExpirationTime, +) { + const mode = workInProgress.mode; + const nextProps = workInProgress.pendingProps; + + // This is used by DevTools to force a boundary to suspend. + if (__DEV__) { + if (shouldSuspend(workInProgress)) { + workInProgress.effectTag |= DidCapture; + } + } + + let suspenseContext: SuspenseContext = suspenseStackCursor.current; + + let nextDidTimeout = false; + const didSuspend = (workInProgress.effectTag & DidCapture) !== NoEffect; + + if ( + didSuspend || + shouldRemainOnFallback( + suspenseContext, + current, + workInProgress, + renderExpirationTime, + ) + ) { + // Something in this boundary's subtree already suspended. Switch to + // rendering the fallback children. + nextDidTimeout = true; + workInProgress.effectTag &= ~DidCapture; + } else { + // Attempting the main content + if ( + current === null || + (current.memoizedState: null | SuspenseState) !== null + ) { + // This is a new mount or this boundary is already showing a fallback state. + // Mark this subtree context as having at least one invisible parent that could + // handle the fallback state. + // Boundaries without fallbacks or should be avoided are not considered since + // they cannot handle preferred fallback states. + if ( + nextProps.fallback !== undefined && + nextProps.unstable_avoidThisFallback !== true + ) { + suspenseContext = addSubtreeSuspenseContext( + suspenseContext, + InvisibleParentSuspenseContext, + ); + } + } + } + + suspenseContext = setDefaultShallowSuspenseContext(suspenseContext); + + pushSuspenseContext(workInProgress, suspenseContext); + + // 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. + + if (current === null) { + // If we're currently hydrating, try to hydrate this boundary. + // But only if this has a fallback. + if (nextProps.fallback !== undefined) { + tryToClaimNextHydratableInstance(workInProgress); + // This could've been a dehydrated suspense component. + if (enableSuspenseServerRenderer) { + const suspenseState: null | SuspenseState = + workInProgress.memoizedState; + if (suspenseState !== null) { + const dehydrated = suspenseState.dehydrated; + if (dehydrated !== null) { + return mountDehydratedSuspenseComponent( + workInProgress, + dehydrated, + renderExpirationTime, + ); + } + } + } + } + + // 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, + ); + primaryChildFragment.return = workInProgress; + + if ((workInProgress.mode & BlockingMode) === NoMode) { + // Outside of blocking mode, we commit the effects from the + // partially completed, timed-out tree, too. + const progressedState: SuspenseState = workInProgress.memoizedState; + const progressedPrimaryChild: Fiber | null = + progressedState !== null + ? (workInProgress.child: any).child + : (workInProgress.child: any); + primaryChildFragment.child = progressedPrimaryChild; + let progressedChild = progressedPrimaryChild; + while (progressedChild !== null) { + progressedChild.return = primaryChildFragment; + progressedChild = progressedChild.sibling; + } + } + + const fallbackChildFragment = createFiberFromFragment( + nextFallbackChildren, + mode, + renderExpirationTime, + null, + ); + fallbackChildFragment.return = workInProgress; + primaryChildFragment.sibling = fallbackChildFragment; + // Skip the primary children, and continue working on the + // fallback children. + workInProgress.memoizedState = mountSuspenseState(renderExpirationTime); + workInProgress.child = primaryChildFragment; + return fallbackChildFragment; + } else { + // Mount the primary children without an intermediate fragment fiber. + const nextPrimaryChildren = nextProps.children; + workInProgress.memoizedState = null; + return (workInProgress.child = 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: null | SuspenseState = current.memoizedState; + if (prevState !== null) { + if (enableSuspenseServerRenderer) { + const dehydrated = prevState.dehydrated; + if (dehydrated !== null) { + if (!didSuspend) { + return updateDehydratedSuspenseComponent( + current, + workInProgress, + dehydrated, + prevState, + renderExpirationTime, + ); + } else if ( + (workInProgress.memoizedState: null | SuspenseState) !== null + ) { + // Something suspended and we should still be in dehydrated mode. + // Leave the existing child in place. + workInProgress.child = current.child; + // The dehydrated completion pass expects this flag to be there + // but the normal suspense pass doesn't. + workInProgress.effectTag |= DidCapture; + return null; + } else { + // Suspended but we should no longer be in dehydrated mode. + // Therefore we now have to render the fallback. 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.return = workInProgress; + + // This is always null since we never want the previous child + // that we're not going to hydrate. + primaryChildFragment.child = null; + + if ((workInProgress.mode & BlockingMode) === NoMode) { + // Outside of blocking mode, we commit the effects from the + // partially completed, timed-out tree, too. + let progressedChild = (primaryChildFragment.child = + workInProgress.child); + while (progressedChild !== null) { + progressedChild.return = primaryChildFragment; + progressedChild = progressedChild.sibling; + } + } else { + // We will have dropped the effect list which contains the deletion. + // We need to reconcile to delete the current child. + reconcileChildFibers( + workInProgress, + current.child, + null, + renderExpirationTime, + ); + } + + // Because primaryChildFragment is a new fiber that we're inserting as the + // parent of a new tree, we need to set its treeBaseDuration. + if (enableProfilerTimer && workInProgress.mode & ProfileMode) { + // treeBaseDuration is the sum of all the child tree base durations. + let treeBaseDuration = 0; + let hiddenChild = primaryChildFragment.child; + while (hiddenChild !== null) { + treeBaseDuration += hiddenChild.treeBaseDuration; + hiddenChild = hiddenChild.sibling; + } + primaryChildFragment.treeBaseDuration = treeBaseDuration; + } + + // Create a fragment from the fallback children, too. + const fallbackChildFragment = createFiberFromFragment( + nextFallbackChildren, + mode, + renderExpirationTime, + null, + ); + fallbackChildFragment.return = workInProgress; + primaryChildFragment.sibling = fallbackChildFragment; + fallbackChildFragment.effectTag |= Placement; + primaryChildFragment.childExpirationTime = getRemainingWorkInPrimaryTree( + current, + workInProgress, + renderExpirationTime, + ); + workInProgress.memoizedState = updateSuspenseState( + current.memoizedState, + renderExpirationTime, + ); + workInProgress.child = primaryChildFragment; + + // Skip the primary children, and continue working on the + // fallback children. + return fallbackChildFragment; + } + } + } + // 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( + currentPrimaryChildFragment, + currentPrimaryChildFragment.pendingProps, + ); + primaryChildFragment.return = workInProgress; + + if ((workInProgress.mode & BlockingMode) === NoMode) { + // Outside of blocking mode, we commit the effects from the + // partially completed, timed-out tree, too. + const progressedState: SuspenseState = workInProgress.memoizedState; + const progressedPrimaryChild: Fiber | null = + progressedState !== null + ? (workInProgress.child: any).child + : (workInProgress.child: any); + if (progressedPrimaryChild !== currentPrimaryChildFragment.child) { + primaryChildFragment.child = progressedPrimaryChild; + let progressedChild = progressedPrimaryChild; + while (progressedChild !== null) { + progressedChild.return = primaryChildFragment; + progressedChild = progressedChild.sibling; + } + } + } + + // Because primaryChildFragment is a new fiber that we're inserting as the + // parent of a new tree, we need to set its treeBaseDuration. + if (enableProfilerTimer && workInProgress.mode & ProfileMode) { + // treeBaseDuration is the sum of all the child tree base durations. + let treeBaseDuration = 0; + let hiddenChild = primaryChildFragment.child; + while (hiddenChild !== null) { + treeBaseDuration += hiddenChild.treeBaseDuration; + hiddenChild = hiddenChild.sibling; + } + primaryChildFragment.treeBaseDuration = treeBaseDuration; + } + + // Clone the fallback child fragment, too. These we'll continue + // working on. + const fallbackChildFragment = createWorkInProgress( + currentFallbackChildFragment, + nextFallbackChildren, + ); + fallbackChildFragment.return = workInProgress; + primaryChildFragment.sibling = fallbackChildFragment; + primaryChildFragment.childExpirationTime = getRemainingWorkInPrimaryTree( + current, + workInProgress, + renderExpirationTime, + ); + // Skip the primary children, and continue working on the + // fallback children. + workInProgress.memoizedState = updateSuspenseState( + current.memoizedState, + renderExpirationTime, + ); + workInProgress.child = primaryChildFragment; + return fallbackChildFragment; + } 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 primaryChild = reconcileChildFibers( + workInProgress, + currentPrimaryChild, + nextPrimaryChildren, + renderExpirationTime, + ); + + // If this render doesn't suspend, we need to delete the fallback + // children. Wait until the complete phase, after we've confirmed the + // fallback is no longer needed. + // TODO: Would it be better to store the fallback fragment on + // the stateNode? + + // Continue rendering the children, like we normally do. + workInProgress.memoizedState = null; + return (workInProgress.child = primaryChild); + } + } else { + // The current tree has not already timed out. That means the primary + // children are not wrapped in a fragment fiber. + const currentPrimaryChild = current.child; + 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.return = workInProgress; + primaryChildFragment.child = currentPrimaryChild; + if (currentPrimaryChild !== null) { + currentPrimaryChild.return = primaryChildFragment; + } + + // Even though we're creating a new fiber, there are no new children, + // because we're reusing an already mounted tree. So we don't need to + // schedule a placement. + // primaryChildFragment.effectTag |= Placement; + + if ((workInProgress.mode & BlockingMode) === NoMode) { + // Outside of blocking mode, we commit the effects from the + // partially completed, timed-out tree, too. + const progressedState: SuspenseState = workInProgress.memoizedState; + const progressedPrimaryChild: Fiber | null = + progressedState !== null + ? (workInProgress.child: any).child + : (workInProgress.child: any); + primaryChildFragment.child = progressedPrimaryChild; + let progressedChild = progressedPrimaryChild; + while (progressedChild !== null) { + progressedChild.return = primaryChildFragment; + progressedChild = progressedChild.sibling; + } + } + + // Because primaryChildFragment is a new fiber that we're inserting as the + // parent of a new tree, we need to set its treeBaseDuration. + if (enableProfilerTimer && workInProgress.mode & ProfileMode) { + // treeBaseDuration is the sum of all the child tree base durations. + let treeBaseDuration = 0; + let hiddenChild = primaryChildFragment.child; + while (hiddenChild !== null) { + treeBaseDuration += hiddenChild.treeBaseDuration; + hiddenChild = hiddenChild.sibling; + } + primaryChildFragment.treeBaseDuration = treeBaseDuration; + } + + // Create a fragment from the fallback children, too. + const fallbackChildFragment = createFiberFromFragment( + nextFallbackChildren, + mode, + renderExpirationTime, + null, + ); + fallbackChildFragment.return = workInProgress; + primaryChildFragment.sibling = fallbackChildFragment; + fallbackChildFragment.effectTag |= Placement; + primaryChildFragment.childExpirationTime = getRemainingWorkInPrimaryTree( + current, + workInProgress, + renderExpirationTime, + ); + // Skip the primary children, and continue working on the + // fallback children. + workInProgress.memoizedState = mountSuspenseState(renderExpirationTime); + workInProgress.child = primaryChildFragment; + return fallbackChildFragment; + } else { + // Still haven't timed out. Continue rendering the children, like we + // normally do. + workInProgress.memoizedState = null; + const nextPrimaryChildren = nextProps.children; + return (workInProgress.child = reconcileChildFibers( + workInProgress, + currentPrimaryChild, + nextPrimaryChildren, + renderExpirationTime, + )); + } + } + } +} + +function retrySuspenseComponentWithoutHydrating( + current: Fiber, + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +) { + // We're now not suspended nor dehydrated. + workInProgress.memoizedState = null; + // Retry with the full children. + const nextProps = workInProgress.pendingProps; + const nextChildren = nextProps.children; + // This will ensure that the children get Placement effects and + // that the old child gets a Deletion effect. + // We could also call forceUnmountCurrentAndReconcile. + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); + return workInProgress.child; +} + +function mountDehydratedSuspenseComponent( + workInProgress: Fiber, + suspenseInstance: SuspenseInstance, + renderExpirationTime: ExpirationTime, +): null | Fiber { + // During the first pass, we'll bail out and not drill into the children. + // Instead, we'll leave the content in place and try to hydrate it later. + if ((workInProgress.mode & BlockingMode) === NoMode) { + if (__DEV__) { + console.error( + 'Cannot hydrate Suspense in legacy mode. Switch from ' + + 'ReactDOM.hydrate(element, container) to ' + + 'ReactDOM.createBlockingRoot(container, { hydrate: true })' + + '.render(element) or remove the Suspense components from ' + + 'the server rendered components.', + ); + } + workInProgress.expirationTime = Sync; + } else if (isSuspenseInstanceFallback(suspenseInstance)) { + // This is a client-only boundary. Since we won't get any content from the server + // for this, we need to schedule that at a higher priority based on when it would + // have timed out. In theory we could render it in this pass but it would have the + // wrong priority associated with it and will prevent hydration of parent path. + // Instead, we'll leave work left on it to render it in a separate commit. + + // TODO This time should be the time at which the server rendered response that is + // a parent to this boundary was displayed. However, since we currently don't have + // a protocol to transfer that time, we'll just estimate it by using the current + // time. This will mean that Suspense timeouts are slightly shifted to later than + // they should be. + const serverDisplayTime = requestCurrentTimeForUpdate(); + // Schedule a normal pri update to render this content. + const newExpirationTime = computeAsyncExpiration(serverDisplayTime); + if (enableSchedulerTracing) { + markSpawnedWork(newExpirationTime); + } + workInProgress.expirationTime = newExpirationTime; + } else { + // We'll continue hydrating the rest at offscreen priority since we'll already + // be showing the right content coming from the server, it is no rush. + workInProgress.expirationTime = Never; + if (enableSchedulerTracing) { + markSpawnedWork(Never); + } + } + return null; +} + +function updateDehydratedSuspenseComponent( + current: Fiber, + workInProgress: Fiber, + suspenseInstance: SuspenseInstance, + suspenseState: SuspenseState, + renderExpirationTime: ExpirationTime, +): null | Fiber { + // We should never be hydrating at this point because it is the first pass, + // but after we've already committed once. + warnIfHydrating(); + + if ((workInProgress.mode & BlockingMode) === NoMode) { + return retrySuspenseComponentWithoutHydrating( + current, + workInProgress, + renderExpirationTime, + ); + } + + if (isSuspenseInstanceFallback(suspenseInstance)) { + // This boundary is in a permanent fallback state. In this case, we'll never + // get an update and we'll never be able to hydrate the final content. Let's just try the + // client side render instead. + return retrySuspenseComponentWithoutHydrating( + current, + workInProgress, + renderExpirationTime, + ); + } + // We use childExpirationTime to indicate that a child might depend on context, so if + // any context has changed, we need to treat is as if the input might have changed. + const hasContextChanged = current.childExpirationTime >= renderExpirationTime; + if (didReceiveUpdate || hasContextChanged) { + // This boundary has changed since the first render. This means that we are now unable to + // hydrate it. We might still be able to hydrate it using an earlier expiration time, if + // we are rendering at lower expiration than sync. + if (renderExpirationTime < Sync) { + if (suspenseState.retryTime <= renderExpirationTime) { + // This render is even higher pri than we've seen before, let's try again + // at even higher pri. + const attemptHydrationAtExpirationTime = renderExpirationTime + 1; + suspenseState.retryTime = attemptHydrationAtExpirationTime; + scheduleUpdateOnFiber(current, attemptHydrationAtExpirationTime); + // TODO: Early abort this render. + } else { + // We have already tried to ping at a higher priority than we're rendering with + // so if we got here, we must have failed to hydrate at those levels. We must + // now give up. Instead, we're going to delete the whole subtree and instead inject + // a new real Suspense boundary to take its place, which may render content + // or fallback. This might suspend for a while and if it does we might still have + // an opportunity to hydrate before this pass commits. + } + } + // If we have scheduled higher pri work above, this will probably just abort the render + // since we now have higher priority work, but in case it doesn't, we need to prepare to + // render something, if we time out. Even if that requires us to delete everything and + // skip hydration. + // Delay having to do this as long as the suspense timeout allows us. + renderDidSuspendDelayIfPossible(); + return retrySuspenseComponentWithoutHydrating( + current, + workInProgress, + renderExpirationTime, + ); + } else if (isSuspenseInstancePending(suspenseInstance)) { + // This component is still pending more data from the server, so we can't hydrate its + // content. We treat it as if this component suspended itself. It might seem as if + // we could just try to render it client-side instead. However, this will perform a + // lot of unnecessary work and is unlikely to complete since it often will suspend + // on missing data anyway. Additionally, the server might be able to render more + // than we can on the client yet. In that case we'd end up with more fallback states + // on the client than if we just leave it alone. If the server times out or errors + // these should update this boundary to the permanent Fallback state instead. + // Mark it as having captured (i.e. suspended). + workInProgress.effectTag |= DidCapture; + // Leave the child in place. I.e. the dehydrated fragment. + workInProgress.child = current.child; + // Register a callback to retry this boundary once the server has sent the result. + registerSuspenseInstanceRetry( + suspenseInstance, + retryDehydratedSuspenseBoundary.bind(null, current), + ); + return null; + } else { + // This is the first attempt. + reenterHydrationStateFromDehydratedSuspenseInstance( + workInProgress, + suspenseInstance, + ); + const nextProps = workInProgress.pendingProps; + const nextChildren = nextProps.children; + const child = mountChildFibers( + workInProgress, + null, + nextChildren, + renderExpirationTime, + ); + let node = child; + while (node) { + // Mark each child as hydrating. This is a fast path to know whether this + // tree is part of a hydrating tree. This is used to determine if a child + // node has fully mounted yet, and for scheduling event replaying. + // Conceptually this is similar to Placement in that a new subtree is + // inserted into the React tree here. It just happens to not need DOM + // mutations because it already exists. + node.effectTag |= Hydrating; + node = node.sibling; + } + workInProgress.child = child; + return workInProgress.child; + } +} + +function scheduleWorkOnFiber( + fiber: Fiber, + renderExpirationTime: ExpirationTime, +) { + if (fiber.expirationTime < renderExpirationTime) { + fiber.expirationTime = renderExpirationTime; + } + const alternate = fiber.alternate; + if (alternate !== null && alternate.expirationTime < renderExpirationTime) { + alternate.expirationTime = renderExpirationTime; + } + scheduleWorkOnParentPath(fiber.return, renderExpirationTime); +} + +function propagateSuspenseContextChange( + workInProgress: Fiber, + firstChild: null | Fiber, + renderExpirationTime: ExpirationTime, +): void { + // Mark any Suspense boundaries with fallbacks as having work to do. + // If they were previously forced into fallbacks, they may now be able + // to unblock. + let node = firstChild; + while (node !== null) { + if (node.tag === SuspenseComponent) { + const state: SuspenseState | null = node.memoizedState; + if (state !== null) { + scheduleWorkOnFiber(node, renderExpirationTime); + } + } else if (node.tag === SuspenseListComponent) { + // If the tail is hidden there might not be an Suspense boundaries + // to schedule work on. In this case we have to schedule it on the + // list itself. + // We don't have to traverse to the children of the list since + // the list will propagate the change when it rerenders. + scheduleWorkOnFiber(node, renderExpirationTime); + } 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; + } +} + +function findLastContentRow(firstChild: null | Fiber): null | Fiber { + // This is going to find the last row among these children that is already + // showing content on the screen, as opposed to being in fallback state or + // new. If a row has multiple Suspense boundaries, any of them being in the + // fallback state, counts as the whole row being in a fallback state. + // Note that the "rows" will be workInProgress, but any nested children + // will still be current since we haven't rendered them yet. The mounted + // order may not be the same as the new order. We use the new order. + let row = firstChild; + let lastContentRow: null | Fiber = null; + while (row !== null) { + const currentRow = row.alternate; + // New rows can't be content rows. + if (currentRow !== null && findFirstSuspended(currentRow) === null) { + lastContentRow = row; + } + row = row.sibling; + } + return lastContentRow; +} + +type SuspenseListRevealOrder = 'forwards' | 'backwards' | 'together' | void; + +function validateRevealOrder(revealOrder: SuspenseListRevealOrder) { + if (__DEV__) { + if ( + revealOrder !== undefined && + revealOrder !== 'forwards' && + revealOrder !== 'backwards' && + revealOrder !== 'together' && + !didWarnAboutRevealOrder[revealOrder] + ) { + didWarnAboutRevealOrder[revealOrder] = true; + if (typeof revealOrder === 'string') { + switch (revealOrder.toLowerCase()) { + case 'together': + case 'forwards': + case 'backwards': { + console.error( + '"%s" is not a valid value for revealOrder on . ' + + 'Use lowercase "%s" instead.', + revealOrder, + revealOrder.toLowerCase(), + ); + break; + } + case 'forward': + case 'backward': { + console.error( + '"%s" is not a valid value for revealOrder on . ' + + 'React uses the -s suffix in the spelling. Use "%ss" instead.', + revealOrder, + revealOrder.toLowerCase(), + ); + break; + } + default: + console.error( + '"%s" is not a supported revealOrder on . ' + + 'Did you mean "together", "forwards" or "backwards"?', + revealOrder, + ); + break; + } + } else { + console.error( + '%s is not a supported value for revealOrder on . ' + + 'Did you mean "together", "forwards" or "backwards"?', + revealOrder, + ); + } + } + } +} + +function validateTailOptions( + tailMode: SuspenseListTailMode, + revealOrder: SuspenseListRevealOrder, +) { + if (__DEV__) { + if (tailMode !== undefined && !didWarnAboutTailOptions[tailMode]) { + if (tailMode !== 'collapsed' && tailMode !== 'hidden') { + didWarnAboutTailOptions[tailMode] = true; + console.error( + '"%s" is not a supported value for tail on . ' + + 'Did you mean "collapsed" or "hidden"?', + tailMode, + ); + } else if (revealOrder !== 'forwards' && revealOrder !== 'backwards') { + didWarnAboutTailOptions[tailMode] = true; + console.error( + ' is only valid if revealOrder is ' + + '"forwards" or "backwards". ' + + 'Did you mean to specify revealOrder="forwards"?', + tailMode, + ); + } + } + } +} + +function validateSuspenseListNestedChild(childSlot: mixed, index: number) { + if (__DEV__) { + const isArray = Array.isArray(childSlot); + const isIterable = + !isArray && typeof getIteratorFn(childSlot) === 'function'; + if (isArray || isIterable) { + const type = isArray ? 'array' : 'iterable'; + console.error( + 'A nested %s was passed to row #%s in . Wrap it in ' + + 'an additional SuspenseList to configure its revealOrder: ' + + ' ... ' + + '{%s} ... ' + + '', + type, + index, + type, + ); + return false; + } + } + return true; +} + +function validateSuspenseListChildren( + children: mixed, + revealOrder: SuspenseListRevealOrder, +) { + if (__DEV__) { + if ( + (revealOrder === 'forwards' || revealOrder === 'backwards') && + children !== undefined && + children !== null && + children !== false + ) { + if (Array.isArray(children)) { + for (let i = 0; i < children.length; i++) { + if (!validateSuspenseListNestedChild(children[i], i)) { + return; + } + } + } else { + const iteratorFn = getIteratorFn(children); + if (typeof iteratorFn === 'function') { + const childrenIterator = iteratorFn.call(children); + if (childrenIterator) { + let step = childrenIterator.next(); + let i = 0; + for (; !step.done; step = childrenIterator.next()) { + if (!validateSuspenseListNestedChild(step.value, i)) { + return; + } + i++; + } + } + } else { + console.error( + 'A single row was passed to a . ' + + 'This is not useful since it needs multiple rows. ' + + 'Did you mean to pass multiple children or an array?', + revealOrder, + ); + } + } + } + } +} + +function initSuspenseListRenderState( + workInProgress: Fiber, + isBackwards: boolean, + tail: null | Fiber, + lastContentRow: null | Fiber, + tailMode: SuspenseListTailMode, + lastEffectBeforeRendering: null | Fiber, +): void { + const renderState: null | SuspenseListRenderState = + workInProgress.memoizedState; + if (renderState === null) { + workInProgress.memoizedState = ({ + isBackwards: isBackwards, + rendering: null, + renderingStartTime: 0, + last: lastContentRow, + tail: tail, + tailExpiration: 0, + tailMode: tailMode, + lastEffect: lastEffectBeforeRendering, + }: SuspenseListRenderState); + } else { + // We can reuse the existing object from previous renders. + renderState.isBackwards = isBackwards; + renderState.rendering = null; + renderState.renderingStartTime = 0; + renderState.last = lastContentRow; + renderState.tail = tail; + renderState.tailExpiration = 0; + renderState.tailMode = tailMode; + renderState.lastEffect = lastEffectBeforeRendering; + } +} + +// This can end up rendering this component multiple passes. +// The first pass splits the children fibers into two sets. A head and tail. +// We first render the head. If anything is in fallback state, we do another +// pass through beginWork to rerender all children (including the tail) with +// the force suspend context. If the first render didn't have anything in +// in fallback state. Then we render each row in the tail one-by-one. +// That happens in the completeWork phase without going back to beginWork. +function updateSuspenseListComponent( + current: Fiber | null, + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +) { + const nextProps = workInProgress.pendingProps; + const revealOrder: SuspenseListRevealOrder = nextProps.revealOrder; + const tailMode: SuspenseListTailMode = nextProps.tail; + const newChildren = nextProps.children; + + validateRevealOrder(revealOrder); + validateTailOptions(tailMode, revealOrder); + validateSuspenseListChildren(newChildren, revealOrder); + + reconcileChildren(current, workInProgress, newChildren, renderExpirationTime); + + let suspenseContext: SuspenseContext = suspenseStackCursor.current; + + const shouldForceFallback = hasSuspenseContext( + suspenseContext, + (ForceSuspenseFallback: SuspenseContext), + ); + if (shouldForceFallback) { + suspenseContext = setShallowSuspenseContext( + suspenseContext, + ForceSuspenseFallback, + ); + workInProgress.effectTag |= DidCapture; + } else { + const didSuspendBefore = + current !== null && (current.effectTag & DidCapture) !== NoEffect; + if (didSuspendBefore) { + // If we previously forced a fallback, we need to schedule work + // on any nested boundaries to let them know to try to render + // again. This is the same as context updating. + propagateSuspenseContextChange( + workInProgress, + workInProgress.child, + renderExpirationTime, + ); + } + suspenseContext = setDefaultShallowSuspenseContext(suspenseContext); + } + pushSuspenseContext(workInProgress, suspenseContext); + + if ((workInProgress.mode & BlockingMode) === NoMode) { + // Outside of blocking mode, SuspenseList doesn't work so we just + // use make it a noop by treating it as the default revealOrder. + workInProgress.memoizedState = null; + } else { + switch (revealOrder) { + case 'forwards': { + const lastContentRow = findLastContentRow(workInProgress.child); + let tail; + if (lastContentRow === null) { + // The whole list is part of the tail. + // TODO: We could fast path by just rendering the tail now. + tail = workInProgress.child; + workInProgress.child = null; + } else { + // Disconnect the tail rows after the content row. + // We're going to render them separately later. + tail = lastContentRow.sibling; + lastContentRow.sibling = null; + } + initSuspenseListRenderState( + workInProgress, + false, // isBackwards + tail, + lastContentRow, + tailMode, + workInProgress.lastEffect, + ); + break; + } + case 'backwards': { + // We're going to find the first row that has existing content. + // At the same time we're going to reverse the list of everything + // we pass in the meantime. That's going to be our tail in reverse + // order. + let tail = null; + let row = workInProgress.child; + workInProgress.child = null; + while (row !== null) { + const currentRow = row.alternate; + // New rows can't be content rows. + if (currentRow !== null && findFirstSuspended(currentRow) === null) { + // This is the beginning of the main content. + workInProgress.child = row; + break; + } + const nextRow = row.sibling; + row.sibling = tail; + tail = row; + row = nextRow; + } + // TODO: If workInProgress.child is null, we can continue on the tail immediately. + initSuspenseListRenderState( + workInProgress, + true, // isBackwards + tail, + null, // last + tailMode, + workInProgress.lastEffect, + ); + break; + } + case 'together': { + initSuspenseListRenderState( + workInProgress, + false, // isBackwards + null, // tail + null, // last + undefined, + workInProgress.lastEffect, + ); + break; + } + default: { + // The default reveal order is the same as not having + // a boundary. + workInProgress.memoizedState = null; + } + } + } + return workInProgress.child; +} + +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 = workInProgress.type; + const context: ReactContext = 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'); + } + } + + 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 = 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; + console.error( + 'Rendering directly is not supported and will be removed in ' + + 'a future major release. Did you mean to render instead?', + ); + } + } + } else { + context = (context: any)._context; + } + } + const newProps = workInProgress.pendingProps; + const render = newProps.children; + + if (__DEV__) { + if (typeof render !== 'function') { + console.error( + '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; + setIsRendering(true); + newChildren = render(newValue); + setIsRendering(false); + } else { + newChildren = render(newValue); + } + + // React DevTools reads this flag. + workInProgress.effectTag |= PerformedWork; + reconcileChildren(current, workInProgress, newChildren, renderExpirationTime); + return workInProgress.child; +} + +function updateFundamentalComponent( + current, + workInProgress, + renderExpirationTime, +) { + const fundamentalImpl = workInProgress.type.impl; + if (fundamentalImpl.reconcileChildren === false) { + return null; + } + const nextProps = workInProgress.pendingProps; + const nextChildren = nextProps.children; + + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); + return workInProgress.child; +} + +function updateScopeComponent(current, workInProgress, renderExpirationTime) { + const nextProps = workInProgress.pendingProps; + const nextChildren = nextProps.children; + + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); + return workInProgress.child; +} + +export function markWorkInProgressReceivedUpdate() { + didReceiveUpdate = true; +} + +function bailoutOnAlreadyFinishedWork( + current: Fiber | null, + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +): Fiber | null { + if (current !== null) { + // Reuse previous dependencies + workInProgress.dependencies = current.dependencies; + } + + if (enableProfilerTimer) { + // Don't update "base" render times for bailouts. + stopProfilerTimerIfRunning(workInProgress); + } + + const updateExpirationTime = workInProgress.expirationTime; + if (updateExpirationTime !== NoWork) { + markUnprocessedUpdateTime(updateExpirationTime); + } + + // Check if the children have any pending work. + const childExpirationTime = workInProgress.childExpirationTime; + if (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 remountFiber( + current: Fiber, + oldWorkInProgress: Fiber, + newWorkInProgress: Fiber, +): Fiber | null { + if (__DEV__) { + const returnFiber = oldWorkInProgress.return; + if (returnFiber === null) { + throw new Error('Cannot swap the root fiber.'); + } + + // Disconnect from the old current. + // It will get deleted. + current.alternate = null; + oldWorkInProgress.alternate = null; + + // Connect to the new tree. + newWorkInProgress.index = oldWorkInProgress.index; + newWorkInProgress.sibling = oldWorkInProgress.sibling; + newWorkInProgress.return = oldWorkInProgress.return; + newWorkInProgress.ref = oldWorkInProgress.ref; + + // Replace the child/sibling pointers above it. + if (oldWorkInProgress === returnFiber.child) { + returnFiber.child = newWorkInProgress; + } else { + let prevSibling = returnFiber.child; + if (prevSibling === null) { + throw new Error('Expected parent to have a child.'); + } + while (prevSibling.sibling !== oldWorkInProgress) { + prevSibling = prevSibling.sibling; + if (prevSibling === null) { + throw new Error('Expected to find the previous sibling.'); + } + } + prevSibling.sibling = newWorkInProgress; + } + + // Delete the old fiber and place the new one. + // Since the old fiber is disconnected, we have to schedule it manually. + const last = returnFiber.lastEffect; + if (last !== null) { + last.nextEffect = current; + returnFiber.lastEffect = current; + } else { + returnFiber.firstEffect = returnFiber.lastEffect = current; + } + current.nextEffect = null; + current.effectTag = Deletion; + + newWorkInProgress.effectTag |= Placement; + + // Restart work from the new fiber. + return newWorkInProgress; + } else { + throw new Error( + 'Did not expect this call in production. ' + + 'This is a bug in React. Please file an issue.', + ); + } +} + +function beginWork( + current: Fiber | null, + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +): Fiber | null { + const updateExpirationTime = workInProgress.expirationTime; + + if (__DEV__) { + if (workInProgress._debugNeedsRemount && current !== null) { + // This will restart the begin phase with a new fiber. + return remountFiber( + current, + workInProgress, + createFiberFromTypeAndProps( + workInProgress.type, + workInProgress.key, + workInProgress.pendingProps, + workInProgress._debugOwner || null, + workInProgress.mode, + workInProgress.expirationTime, + ), + ); + } + } + + if (current !== null) { + const oldProps = current.memoizedProps; + const newProps = workInProgress.pendingProps; + + if ( + oldProps !== newProps || + hasLegacyContextChanged() || + // Force a re-render if the implementation changed due to hot reload: + (__DEV__ ? workInProgress.type !== current.type : false) + ) { + // If props or context changed, mark the fiber as having performed work. + // This may be unset if the props are determined to be equal later (memo). + didReceiveUpdate = true; + } else if (updateExpirationTime < renderExpirationTime) { + didReceiveUpdate = false; + // 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); + if ( + workInProgress.mode & ConcurrentMode && + renderExpirationTime !== Never && + shouldDeprioritizeSubtree(workInProgress.type, newProps) + ) { + if (enableSchedulerTracing) { + markSpawnedWork(Never); + } + // Schedule this fiber to re-render at offscreen priority. Then bailout. + workInProgress.expirationTime = workInProgress.childExpirationTime = Never; + return null; + } + break; + case ClassComponent: { + const Component = workInProgress.type; + 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) { + // Profiler should only call onRender when one of its descendants actually rendered. + const hasChildWork = + workInProgress.childExpirationTime >= renderExpirationTime; + if (hasChildWork) { + workInProgress.effectTag |= Update; + } + + // Reset effect durations for the next eventual effect phase. + // These are reset during render to allow the DevTools commit hook a chance to read them, + const stateNode = workInProgress.stateNode; + stateNode.effectDuration = 0; + stateNode.passiveEffectDuration = 0; + } + break; + case SuspenseComponent: { + const state: SuspenseState | null = workInProgress.memoizedState; + if (state !== null) { + if (enableSuspenseServerRenderer) { + if (state.dehydrated !== null) { + pushSuspenseContext( + workInProgress, + setDefaultShallowSuspenseContext(suspenseStackCursor.current), + ); + // We know that this component will suspend again because if it has + // been unsuspended it has committed as a resolved Suspense component. + // If it needs to be retried, it should have work scheduled on it. + workInProgress.effectTag |= DidCapture; + break; + } + } + + // If this boundary is currently timed out, we need to decide + // whether to retry the primary children, or to skip over it and + // go straight to the fallback. Check the priority of the primary + // child fragment. + const primaryChildFragment: Fiber = (workInProgress.child: any); + const primaryChildExpirationTime = + primaryChildFragment.childExpirationTime; + if ( + primaryChildExpirationTime !== NoWork && + primaryChildExpirationTime >= renderExpirationTime + ) { + // The primary children have pending work. Use the normal path + // to attempt to render the primary children again. + return updateSuspenseComponent( + current, + workInProgress, + renderExpirationTime, + ); + } else { + // The primary child fragment does not have pending work marked + // on it... + + // ...usually. There's an unfortunate edge case where the fragment + // fiber is not part of the return path of the children, so when + // an update happens, the fragment doesn't get marked during + // setState. This is something we should consider addressing when + // we refactor the Fiber data structure. (There's a test with more + // details; to find it, comment out the following block and see + // which one fails.) + // + // As a workaround, we need to recompute the `childExpirationTime` + // by bubbling it up from the next level of children. This is + // based on similar logic in `resetChildExpirationTime`. + let primaryChild = primaryChildFragment.child; + while (primaryChild !== null) { + const childUpdateExpirationTime = primaryChild.expirationTime; + const childChildExpirationTime = + primaryChild.childExpirationTime; + if ( + (childUpdateExpirationTime !== NoWork && + childUpdateExpirationTime >= renderExpirationTime) || + (childChildExpirationTime !== NoWork && + childChildExpirationTime >= renderExpirationTime) + ) { + // Found a child with an update with sufficient priority. + // Use the normal path to render the primary children again. + return updateSuspenseComponent( + current, + workInProgress, + renderExpirationTime, + ); + } + primaryChild = primaryChild.sibling; + } + + pushSuspenseContext( + workInProgress, + setDefaultShallowSuspenseContext(suspenseStackCursor.current), + ); + // The primary children do not have pending work with sufficient + // priority. Bailout. + const child = bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); + if (child !== null) { + // The fallback children have pending work. Skip over the + // primary children and work on the fallback. + return child.sibling; + } else { + return null; + } + } + } else { + pushSuspenseContext( + workInProgress, + setDefaultShallowSuspenseContext(suspenseStackCursor.current), + ); + } + break; + } + case SuspenseListComponent: { + const didSuspendBefore = + (current.effectTag & DidCapture) !== NoEffect; + + const hasChildWork = + workInProgress.childExpirationTime >= renderExpirationTime; + + if (didSuspendBefore) { + if (hasChildWork) { + // If something was in fallback state last time, and we have all the + // same children then we're still in progressive loading state. + // Something might get unblocked by state updates or retries in the + // tree which will affect the tail. So we need to use the normal + // path to compute the correct tail. + return updateSuspenseListComponent( + current, + workInProgress, + renderExpirationTime, + ); + } + // If none of the children had any work, that means that none of + // them got retried so they'll still be blocked in the same way + // as before. We can fast bail out. + workInProgress.effectTag |= DidCapture; + } + + // If nothing suspended before and we're rendering the same children, + // then the tail doesn't matter. Anything new that suspends will work + // in the "together" mode, so we can continue from the state we had. + const renderState = workInProgress.memoizedState; + if (renderState !== null) { + // Reset to the "together" mode in case we've started a different + // update in the past but didn't complete it. + renderState.rendering = null; + renderState.tail = null; + renderState.lastEffect = null; + } + pushSuspenseContext(workInProgress, suspenseStackCursor.current); + + if (hasChildWork) { + break; + } else { + // If none of the children had any work, that means that none of + // them got retried so they'll still be blocked in the same way + // as before. We can fast bail out. + return null; + } + } + } + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); + } else { + // An update was scheduled on this fiber, but there are no new props + // nor legacy context. Set this to false. If an update queue or context + // consumer produces a changed value, it will set this to true. Otherwise, + // the component will assume the children have not changed and bail out. + didReceiveUpdate = false; + } + } else { + didReceiveUpdate = false; + } + + // Before entering the begin phase, clear pending update priority. + // TODO: This assumes that we're about to evaluate the component and process + // the update queue. However, there's an exception: SimpleMemoComponent + // sometimes bails out later in the begin phase. This indicates that we should + // move this assignment out of the common path and into each branch. + workInProgress.expirationTime = NoWork; + + switch (workInProgress.tag) { + case IndeterminateComponent: { + return mountIndeterminateComponent( + current, + workInProgress, + workInProgress.type, + renderExpirationTime, + ); + } + case LazyComponent: { + const elementType = workInProgress.elementType; + return mountLazyComponent( + current, + workInProgress, + elementType, + updateExpirationTime, + renderExpirationTime, + ); + } + case FunctionComponent: { + const Component = workInProgress.type; + const unresolvedProps = workInProgress.pendingProps; + const resolvedProps = + workInProgress.elementType === Component + ? unresolvedProps + : resolveDefaultProps(Component, unresolvedProps); + return updateFunctionComponent( + current, + workInProgress, + Component, + resolvedProps, + renderExpirationTime, + ); + } + case ClassComponent: { + const Component = workInProgress.type; + const unresolvedProps = workInProgress.pendingProps; + const resolvedProps = + workInProgress.elementType === Component + ? unresolvedProps + : resolveDefaultProps(Component, unresolvedProps); + return updateClassComponent( + current, + workInProgress, + Component, + resolvedProps, + renderExpirationTime, + ); + } + 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; + const unresolvedProps = workInProgress.pendingProps; + const resolvedProps = + workInProgress.elementType === type + ? unresolvedProps + : resolveDefaultProps(type, unresolvedProps); + return updateForwardRef( + current, + workInProgress, + type, + resolvedProps, + renderExpirationTime, + ); + } + 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 MemoComponent: { + const type = workInProgress.type; + const unresolvedProps = workInProgress.pendingProps; + // Resolve outer props first, then resolve inner props. + let resolvedProps = resolveDefaultProps(type, unresolvedProps); + if (__DEV__) { + if (workInProgress.type !== workInProgress.elementType) { + const outerPropTypes = type.propTypes; + if (outerPropTypes) { + checkPropTypes( + outerPropTypes, + resolvedProps, // Resolved for outer only + 'prop', + getComponentName(type), + ); + } + } + } + resolvedProps = resolveDefaultProps(type.type, resolvedProps); + return updateMemoComponent( + current, + workInProgress, + type, + resolvedProps, + updateExpirationTime, + renderExpirationTime, + ); + } + case SimpleMemoComponent: { + return updateSimpleMemoComponent( + current, + workInProgress, + workInProgress.type, + workInProgress.pendingProps, + updateExpirationTime, + renderExpirationTime, + ); + } + case IncompleteClassComponent: { + const Component = workInProgress.type; + const unresolvedProps = workInProgress.pendingProps; + const resolvedProps = + workInProgress.elementType === Component + ? unresolvedProps + : resolveDefaultProps(Component, unresolvedProps); + return mountIncompleteClassComponent( + current, + workInProgress, + Component, + resolvedProps, + renderExpirationTime, + ); + } + case SuspenseListComponent: { + return updateSuspenseListComponent( + current, + workInProgress, + renderExpirationTime, + ); + } + case FundamentalComponent: { + if (enableFundamentalAPI) { + return updateFundamentalComponent( + current, + workInProgress, + renderExpirationTime, + ); + } + break; + } + case ScopeComponent: { + if (enableScopeAPI) { + return updateScopeComponent( + current, + workInProgress, + renderExpirationTime, + ); + } + break; + } + case Block: { + if (enableBlocksAPI) { + const block = workInProgress.type; + const props = workInProgress.pendingProps; + return updateBlock( + current, + workInProgress, + block, + props, + renderExpirationTime, + ); + } + 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 {beginWork}; diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.new.js b/packages/react-reconciler/src/ReactFiberClassComponent.new.js new file mode 100644 index 0000000000000..461fd94d5e9dd --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberClassComponent.new.js @@ -0,0 +1,1182 @@ +/** + * 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'; +import type {UpdateQueue} from './ReactUpdateQueue.new'; + +import * as React from 'react'; +import {Update, Snapshot} from './ReactSideEffectTags'; +import { + debugRenderPhaseSideEffectsForStrictMode, + disableLegacyContext, + warnAboutDeprecatedLifecycles, +} from 'shared/ReactFeatureFlags'; +import ReactStrictModeWarnings from './ReactStrictModeWarnings.new'; +import {isMounted} from './ReactFiberTreeReflection'; +import {get as getInstance, set as setInstance} from 'shared/ReactInstanceMap'; +import shallowEqual from 'shared/shallowEqual'; +import getComponentName from 'shared/getComponentName'; +import invariant from 'shared/invariant'; +import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols'; + +import {resolveDefaultProps} from './ReactFiberLazyComponent.new'; +import {StrictMode} from './ReactTypeOfMode'; + +import { + enqueueUpdate, + processUpdateQueue, + checkHasForceUpdateAfterProcessing, + resetHasForceUpdateBeforeProcessing, + createUpdate, + ReplaceState, + ForceUpdate, + initializeUpdateQueue, + cloneUpdateQueue, +} from './ReactUpdateQueue.new'; +import {NoWork} from './ReactFiberExpirationTime'; +import { + cacheContext, + getMaskedContext, + getUnmaskedContext, + hasContextChanged, + emptyContextObject, +} from './ReactFiberContext.new'; +import {readContext} from './ReactFiberNewContext.new'; +import { + requestCurrentTimeForUpdate, + computeExpirationForFiber, + scheduleUpdateOnFiber, +} from './ReactFiberWorkLoop.new'; +import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig'; + +import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev'; + +const fakeInternalInstance = {}; +const isArray = Array.isArray; + +// React.Component uses a shared frozen object by default. +// We'll use it to determine whether we need to initialize legacy refs. +export const emptyRefsObject = new React.Component().refs; + +let didWarnAboutStateAssignmentForComponent; +let didWarnAboutUninitializedState; +let didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate; +let didWarnAboutLegacyLifecyclesAndDerivedState; +let didWarnAboutUndefinedDerivedState; +let warnOnUndefinedDerivedState; +let warnOnInvalidCallback; +let didWarnAboutDirectlyAssigningPropsToState; +let didWarnAboutContextTypeAndContextTypes; +let didWarnAboutInvalidateContextType; + +if (__DEV__) { + didWarnAboutStateAssignmentForComponent = new Set(); + didWarnAboutUninitializedState = new Set(); + didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate = new Set(); + didWarnAboutLegacyLifecyclesAndDerivedState = new Set(); + didWarnAboutDirectlyAssigningPropsToState = new Set(); + didWarnAboutUndefinedDerivedState = new Set(); + didWarnAboutContextTypeAndContextTypes = new Set(); + didWarnAboutInvalidateContextType = new Set(); + + const didWarnOnInvalidCallback = new Set(); + + warnOnInvalidCallback = function(callback: mixed, callerName: string) { + if (callback === null || typeof callback === 'function') { + return; + } + const key = `${callerName}_${(callback: any)}`; + if (!didWarnOnInvalidCallback.has(key)) { + didWarnOnInvalidCallback.add(key); + console.error( + '%s(...): Expected the last optional `callback` argument to be a ' + + 'function. Instead received: %s.', + callerName, + callback, + ); + } + }; + + warnOnUndefinedDerivedState = function(type, partialState) { + if (partialState === undefined) { + const componentName = getComponentName(type) || 'Component'; + if (!didWarnAboutUndefinedDerivedState.has(componentName)) { + didWarnAboutUndefinedDerivedState.add(componentName); + console.error( + '%s.getDerivedStateFromProps(): A valid state object (or null) must be returned. ' + + 'You have returned undefined.', + componentName, + ); + } + } + }; + + // This is so gross but it's at least non-critical and can be removed if + // it causes problems. This is meant to give a nicer error message for + // ReactDOM15.unstable_renderSubtreeIntoContainer(reactDOM16Component, + // ...)) which otherwise throws a "_processChildContext is not a function" + // exception. + Object.defineProperty(fakeInternalInstance, '_processChildContext', { + enumerable: false, + value: function() { + invariant( + false, + '_processChildContext is not available in React 16+. This likely ' + + 'means you have multiple copies of React and are attempting to nest ' + + 'a React 15 tree inside a React 16 tree using ' + + "unstable_renderSubtreeIntoContainer, which isn't supported. Try " + + 'to make sure you have only one copy of React (and ideally, switch ' + + 'to ReactDOM.createPortal).', + ); + }, + }); + Object.freeze(fakeInternalInstance); +} + +export function applyDerivedStateFromProps( + workInProgress: Fiber, + ctor: any, + getDerivedStateFromProps: (props: any, state: any) => any, + nextProps: any, +) { + const prevState = workInProgress.memoizedState; + + if (__DEV__) { + if ( + debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode + ) { + disableLogs(); + try { + // Invoke the function an extra time to help detect side-effects. + getDerivedStateFromProps(nextProps, prevState); + } finally { + reenableLogs(); + } + } + } + + const partialState = getDerivedStateFromProps(nextProps, prevState); + + if (__DEV__) { + warnOnUndefinedDerivedState(ctor, partialState); + } + // Merge the partial state and the previous state. + const memoizedState = + partialState === null || partialState === undefined + ? prevState + : Object.assign({}, prevState, partialState); + workInProgress.memoizedState = memoizedState; + + // Once the update queue is empty, persist the derived state onto the + // base state. + if (workInProgress.expirationTime === NoWork) { + // Queue is always non-null for classes + const updateQueue: UpdateQueue = (workInProgress.updateQueue: any); + updateQueue.baseState = memoizedState; + } +} + +const classComponentUpdater = { + isMounted, + enqueueSetState(inst, payload, callback) { + const fiber = getInstance(inst); + const currentTime = requestCurrentTimeForUpdate(); + const suspenseConfig = requestCurrentSuspenseConfig(); + const expirationTime = computeExpirationForFiber( + currentTime, + fiber, + suspenseConfig, + ); + + const update = createUpdate(expirationTime, suspenseConfig); + update.payload = payload; + if (callback !== undefined && callback !== null) { + if (__DEV__) { + warnOnInvalidCallback(callback, 'setState'); + } + update.callback = callback; + } + + enqueueUpdate(fiber, update); + scheduleUpdateOnFiber(fiber, expirationTime); + }, + enqueueReplaceState(inst, payload, callback) { + const fiber = getInstance(inst); + const currentTime = requestCurrentTimeForUpdate(); + const suspenseConfig = requestCurrentSuspenseConfig(); + const expirationTime = computeExpirationForFiber( + currentTime, + fiber, + suspenseConfig, + ); + + const update = createUpdate(expirationTime, suspenseConfig); + update.tag = ReplaceState; + update.payload = payload; + + if (callback !== undefined && callback !== null) { + if (__DEV__) { + warnOnInvalidCallback(callback, 'replaceState'); + } + update.callback = callback; + } + + enqueueUpdate(fiber, update); + scheduleUpdateOnFiber(fiber, expirationTime); + }, + enqueueForceUpdate(inst, callback) { + const fiber = getInstance(inst); + const currentTime = requestCurrentTimeForUpdate(); + const suspenseConfig = requestCurrentSuspenseConfig(); + const expirationTime = computeExpirationForFiber( + currentTime, + fiber, + suspenseConfig, + ); + + const update = createUpdate(expirationTime, suspenseConfig); + update.tag = ForceUpdate; + + if (callback !== undefined && callback !== null) { + if (__DEV__) { + warnOnInvalidCallback(callback, 'forceUpdate'); + } + update.callback = callback; + } + + enqueueUpdate(fiber, update); + scheduleUpdateOnFiber(fiber, expirationTime); + }, +}; + +function checkShouldComponentUpdate( + workInProgress, + ctor, + oldProps, + newProps, + oldState, + newState, + nextContext, +) { + const instance = workInProgress.stateNode; + if (typeof instance.shouldComponentUpdate === 'function') { + if (__DEV__) { + if ( + debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode + ) { + disableLogs(); + try { + // Invoke the function an extra time to help detect side-effects. + instance.shouldComponentUpdate(newProps, newState, nextContext); + } finally { + reenableLogs(); + } + } + } + const shouldUpdate = instance.shouldComponentUpdate( + newProps, + newState, + nextContext, + ); + + if (__DEV__) { + if (shouldUpdate === undefined) { + console.error( + '%s.shouldComponentUpdate(): Returned undefined instead of a ' + + 'boolean value. Make sure to return true or false.', + getComponentName(ctor) || 'Component', + ); + } + } + + return shouldUpdate; + } + + if (ctor.prototype && ctor.prototype.isPureReactComponent) { + return ( + !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState) + ); + } + + return true; +} + +function checkClassInstance(workInProgress: Fiber, ctor: any, newProps: any) { + const instance = workInProgress.stateNode; + if (__DEV__) { + const name = getComponentName(ctor) || 'Component'; + const renderPresent = instance.render; + + if (!renderPresent) { + if (ctor.prototype && typeof ctor.prototype.render === 'function') { + console.error( + '%s(...): No `render` method found on the returned component ' + + 'instance: did you accidentally return an object from the constructor?', + name, + ); + } else { + console.error( + '%s(...): No `render` method found on the returned component ' + + 'instance: you may have forgotten to define `render`.', + name, + ); + } + } + + if ( + instance.getInitialState && + !instance.getInitialState.isReactClassApproved && + !instance.state + ) { + console.error( + 'getInitialState was defined on %s, a plain JavaScript class. ' + + 'This is only supported for classes created using React.createClass. ' + + 'Did you mean to define a state property instead?', + name, + ); + } + if ( + instance.getDefaultProps && + !instance.getDefaultProps.isReactClassApproved + ) { + console.error( + 'getDefaultProps was defined on %s, a plain JavaScript class. ' + + 'This is only supported for classes created using React.createClass. ' + + 'Use a static property to define defaultProps instead.', + name, + ); + } + if (instance.propTypes) { + console.error( + 'propTypes was defined as an instance property on %s. Use a static ' + + 'property to define propTypes instead.', + name, + ); + } + if (instance.contextType) { + console.error( + 'contextType was defined as an instance property on %s. Use a static ' + + 'property to define contextType instead.', + name, + ); + } + + if (disableLegacyContext) { + if (ctor.childContextTypes) { + console.error( + '%s uses the legacy childContextTypes API which is no longer supported. ' + + 'Use React.createContext() instead.', + name, + ); + } + if (ctor.contextTypes) { + console.error( + '%s uses the legacy contextTypes API which is no longer supported. ' + + 'Use React.createContext() with static contextType instead.', + name, + ); + } + } else { + if (instance.contextTypes) { + console.error( + 'contextTypes was defined as an instance property on %s. Use a static ' + + 'property to define contextTypes instead.', + name, + ); + } + + if ( + ctor.contextType && + ctor.contextTypes && + !didWarnAboutContextTypeAndContextTypes.has(ctor) + ) { + didWarnAboutContextTypeAndContextTypes.add(ctor); + console.error( + '%s declares both contextTypes and contextType static properties. ' + + 'The legacy contextTypes property will be ignored.', + name, + ); + } + } + + if (typeof instance.componentShouldUpdate === 'function') { + console.error( + '%s has a method called ' + + 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + + 'The name is phrased as a question because the function is ' + + 'expected to return a value.', + name, + ); + } + if ( + ctor.prototype && + ctor.prototype.isPureReactComponent && + typeof instance.shouldComponentUpdate !== 'undefined' + ) { + console.error( + '%s has a method called shouldComponentUpdate(). ' + + 'shouldComponentUpdate should not be used when extending React.PureComponent. ' + + 'Please extend React.Component if shouldComponentUpdate is used.', + getComponentName(ctor) || 'A pure component', + ); + } + if (typeof instance.componentDidUnmount === 'function') { + console.error( + '%s has a method called ' + + 'componentDidUnmount(). But there is no such lifecycle method. ' + + 'Did you mean componentWillUnmount()?', + name, + ); + } + if (typeof instance.componentDidReceiveProps === 'function') { + console.error( + '%s has a method called ' + + 'componentDidReceiveProps(). But there is no such lifecycle method. ' + + 'If you meant to update the state in response to changing props, ' + + 'use componentWillReceiveProps(). If you meant to fetch data or ' + + 'run side-effects or mutations after React has updated the UI, use componentDidUpdate().', + name, + ); + } + if (typeof instance.componentWillRecieveProps === 'function') { + console.error( + '%s has a method called ' + + 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', + name, + ); + } + if (typeof instance.UNSAFE_componentWillRecieveProps === 'function') { + console.error( + '%s has a method called ' + + 'UNSAFE_componentWillRecieveProps(). Did you mean UNSAFE_componentWillReceiveProps()?', + name, + ); + } + const hasMutatedProps = instance.props !== newProps; + if (instance.props !== undefined && hasMutatedProps) { + console.error( + '%s(...): When calling super() in `%s`, make sure to pass ' + + "up the same props that your component's constructor was passed.", + name, + name, + ); + } + if (instance.defaultProps) { + console.error( + 'Setting defaultProps as an instance property on %s is not supported and will be ignored.' + + ' Instead, define defaultProps as a static property on %s.', + name, + name, + ); + } + + if ( + typeof instance.getSnapshotBeforeUpdate === 'function' && + typeof instance.componentDidUpdate !== 'function' && + !didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate.has(ctor) + ) { + didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate.add(ctor); + console.error( + '%s: getSnapshotBeforeUpdate() should be used with componentDidUpdate(). ' + + 'This component defines getSnapshotBeforeUpdate() only.', + getComponentName(ctor), + ); + } + + if (typeof instance.getDerivedStateFromProps === 'function') { + console.error( + '%s: getDerivedStateFromProps() is defined as an instance method ' + + 'and will be ignored. Instead, declare it as a static method.', + name, + ); + } + if (typeof instance.getDerivedStateFromError === 'function') { + console.error( + '%s: getDerivedStateFromError() is defined as an instance method ' + + 'and will be ignored. Instead, declare it as a static method.', + name, + ); + } + if (typeof ctor.getSnapshotBeforeUpdate === 'function') { + console.error( + '%s: getSnapshotBeforeUpdate() is defined as a static method ' + + 'and will be ignored. Instead, declare it as an instance method.', + name, + ); + } + const state = instance.state; + if (state && (typeof state !== 'object' || isArray(state))) { + console.error('%s.state: must be set to an object or null', name); + } + if ( + typeof instance.getChildContext === 'function' && + typeof ctor.childContextTypes !== 'object' + ) { + console.error( + '%s.getChildContext(): childContextTypes must be defined in order to ' + + 'use getChildContext().', + name, + ); + } + } +} + +function adoptClassInstance(workInProgress: Fiber, instance: any): void { + instance.updater = classComponentUpdater; + workInProgress.stateNode = instance; + // The instance needs access to the fiber so that it can schedule updates + setInstance(instance, workInProgress); + if (__DEV__) { + instance._reactInternalInstance = fakeInternalInstance; + } +} + +function constructClassInstance( + workInProgress: Fiber, + ctor: any, + props: any, +): any { + let isLegacyContextConsumer = false; + let unmaskedContext = emptyContextObject; + let context = emptyContextObject; + const contextType = ctor.contextType; + + if (__DEV__) { + if ('contextType' in ctor) { + const isValid = + // Allow null for conditional declaration + contextType === null || + (contextType !== undefined && + contextType.$$typeof === REACT_CONTEXT_TYPE && + contextType._context === undefined); // Not a + + if (!isValid && !didWarnAboutInvalidateContextType.has(ctor)) { + didWarnAboutInvalidateContextType.add(ctor); + + let addendum = ''; + if (contextType === undefined) { + addendum = + ' However, it is set to undefined. ' + + 'This can be caused by a typo or by mixing up named and default imports. ' + + 'This can also happen due to a circular dependency, so ' + + 'try moving the createContext() call to a separate file.'; + } else if (typeof contextType !== 'object') { + addendum = ' However, it is set to a ' + typeof contextType + '.'; + } else if (contextType.$$typeof === REACT_PROVIDER_TYPE) { + addendum = ' Did you accidentally pass the Context.Provider instead?'; + } else if (contextType._context !== undefined) { + // + addendum = ' Did you accidentally pass the Context.Consumer instead?'; + } else { + addendum = + ' However, it is set to an object with keys {' + + Object.keys(contextType).join(', ') + + '}.'; + } + console.error( + '%s defines an invalid contextType. ' + + 'contextType should point to the Context object returned by React.createContext().%s', + getComponentName(ctor) || 'Component', + addendum, + ); + } + } + } + + if (typeof contextType === 'object' && contextType !== null) { + context = readContext((contextType: any)); + } else if (!disableLegacyContext) { + unmaskedContext = getUnmaskedContext(workInProgress, ctor, true); + const contextTypes = ctor.contextTypes; + isLegacyContextConsumer = + contextTypes !== null && contextTypes !== undefined; + context = isLegacyContextConsumer + ? getMaskedContext(workInProgress, unmaskedContext) + : emptyContextObject; + } + + // Instantiate twice to help detect side-effects. + if (__DEV__) { + if ( + debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode + ) { + disableLogs(); + try { + new ctor(props, context); // eslint-disable-line no-new + } finally { + reenableLogs(); + } + } + } + + const instance = new ctor(props, context); + const state = (workInProgress.memoizedState = + instance.state !== null && instance.state !== undefined + ? instance.state + : null); + adoptClassInstance(workInProgress, instance); + + if (__DEV__) { + if (typeof ctor.getDerivedStateFromProps === 'function' && state === null) { + const componentName = getComponentName(ctor) || 'Component'; + if (!didWarnAboutUninitializedState.has(componentName)) { + didWarnAboutUninitializedState.add(componentName); + console.error( + '`%s` uses `getDerivedStateFromProps` but its initial state is ' + + '%s. This is not recommended. Instead, define the initial state by ' + + 'assigning an object to `this.state` in the constructor of `%s`. ' + + 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.', + componentName, + instance.state === null ? 'null' : 'undefined', + componentName, + ); + } + } + + // If new component APIs are defined, "unsafe" lifecycles won't be called. + // Warn about these lifecycles if they are present. + // Don't warn about react-lifecycles-compat polyfilled methods though. + if ( + typeof ctor.getDerivedStateFromProps === 'function' || + typeof instance.getSnapshotBeforeUpdate === 'function' + ) { + let foundWillMountName = null; + let foundWillReceivePropsName = null; + let foundWillUpdateName = null; + if ( + typeof instance.componentWillMount === 'function' && + instance.componentWillMount.__suppressDeprecationWarning !== true + ) { + foundWillMountName = 'componentWillMount'; + } else if (typeof instance.UNSAFE_componentWillMount === 'function') { + foundWillMountName = 'UNSAFE_componentWillMount'; + } + if ( + typeof instance.componentWillReceiveProps === 'function' && + instance.componentWillReceiveProps.__suppressDeprecationWarning !== true + ) { + foundWillReceivePropsName = 'componentWillReceiveProps'; + } else if ( + typeof instance.UNSAFE_componentWillReceiveProps === 'function' + ) { + foundWillReceivePropsName = 'UNSAFE_componentWillReceiveProps'; + } + if ( + typeof instance.componentWillUpdate === 'function' && + instance.componentWillUpdate.__suppressDeprecationWarning !== true + ) { + foundWillUpdateName = 'componentWillUpdate'; + } else if (typeof instance.UNSAFE_componentWillUpdate === 'function') { + foundWillUpdateName = 'UNSAFE_componentWillUpdate'; + } + if ( + foundWillMountName !== null || + foundWillReceivePropsName !== null || + foundWillUpdateName !== null + ) { + const componentName = getComponentName(ctor) || 'Component'; + const newApiName = + typeof ctor.getDerivedStateFromProps === 'function' + ? 'getDerivedStateFromProps()' + : 'getSnapshotBeforeUpdate()'; + if (!didWarnAboutLegacyLifecyclesAndDerivedState.has(componentName)) { + didWarnAboutLegacyLifecyclesAndDerivedState.add(componentName); + console.error( + 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + + '%s uses %s but also contains the following legacy lifecycles:%s%s%s\n\n' + + 'The above lifecycles should be removed. Learn more about this warning here:\n' + + 'https://fb.me/react-unsafe-component-lifecycles', + componentName, + newApiName, + foundWillMountName !== null ? `\n ${foundWillMountName}` : '', + foundWillReceivePropsName !== null + ? `\n ${foundWillReceivePropsName}` + : '', + foundWillUpdateName !== null ? `\n ${foundWillUpdateName}` : '', + ); + } + } + } + } + + // Cache unmasked context so we can avoid recreating masked context unless necessary. + // ReactFiberContext usually updates this cache but can't for newly-created instances. + if (isLegacyContextConsumer) { + cacheContext(workInProgress, unmaskedContext, context); + } + + return instance; +} + +function callComponentWillMount(workInProgress, instance) { + const oldState = instance.state; + + if (typeof instance.componentWillMount === 'function') { + instance.componentWillMount(); + } + if (typeof instance.UNSAFE_componentWillMount === 'function') { + instance.UNSAFE_componentWillMount(); + } + + if (oldState !== instance.state) { + if (__DEV__) { + console.error( + '%s.componentWillMount(): Assigning directly to this.state is ' + + "deprecated (except inside a component's " + + 'constructor). Use setState instead.', + getComponentName(workInProgress.type) || 'Component', + ); + } + classComponentUpdater.enqueueReplaceState(instance, instance.state, null); + } +} + +function callComponentWillReceiveProps( + workInProgress, + instance, + newProps, + nextContext, +) { + const oldState = instance.state; + if (typeof instance.componentWillReceiveProps === 'function') { + instance.componentWillReceiveProps(newProps, nextContext); + } + if (typeof instance.UNSAFE_componentWillReceiveProps === 'function') { + instance.UNSAFE_componentWillReceiveProps(newProps, nextContext); + } + + if (instance.state !== oldState) { + if (__DEV__) { + const componentName = + getComponentName(workInProgress.type) || 'Component'; + if (!didWarnAboutStateAssignmentForComponent.has(componentName)) { + didWarnAboutStateAssignmentForComponent.add(componentName); + console.error( + '%s.componentWillReceiveProps(): Assigning directly to ' + + "this.state is deprecated (except inside a component's " + + 'constructor). Use setState instead.', + componentName, + ); + } + } + classComponentUpdater.enqueueReplaceState(instance, instance.state, null); + } +} + +// Invokes the mount life-cycles on a previously never rendered instance. +function mountClassInstance( + workInProgress: Fiber, + ctor: any, + newProps: any, + renderExpirationTime: ExpirationTime, +): void { + if (__DEV__) { + checkClassInstance(workInProgress, ctor, newProps); + } + + const instance = workInProgress.stateNode; + instance.props = newProps; + instance.state = workInProgress.memoizedState; + instance.refs = emptyRefsObject; + + initializeUpdateQueue(workInProgress); + + const contextType = ctor.contextType; + if (typeof contextType === 'object' && contextType !== null) { + instance.context = readContext(contextType); + } else if (disableLegacyContext) { + instance.context = emptyContextObject; + } else { + const unmaskedContext = getUnmaskedContext(workInProgress, ctor, true); + instance.context = getMaskedContext(workInProgress, unmaskedContext); + } + + if (__DEV__) { + if (instance.state === newProps) { + const componentName = getComponentName(ctor) || 'Component'; + if (!didWarnAboutDirectlyAssigningPropsToState.has(componentName)) { + didWarnAboutDirectlyAssigningPropsToState.add(componentName); + console.error( + '%s: It is not recommended to assign props directly to state ' + + "because updates to props won't be reflected in state. " + + 'In most cases, it is better to use props directly.', + componentName, + ); + } + } + + if (workInProgress.mode & StrictMode) { + ReactStrictModeWarnings.recordLegacyContextWarning( + workInProgress, + instance, + ); + } + + if (warnAboutDeprecatedLifecycles) { + ReactStrictModeWarnings.recordUnsafeLifecycleWarnings( + workInProgress, + instance, + ); + } + } + + processUpdateQueue(workInProgress, newProps, instance, renderExpirationTime); + instance.state = workInProgress.memoizedState; + + const getDerivedStateFromProps = ctor.getDerivedStateFromProps; + if (typeof getDerivedStateFromProps === 'function') { + applyDerivedStateFromProps( + workInProgress, + ctor, + getDerivedStateFromProps, + newProps, + ); + instance.state = workInProgress.memoizedState; + } + + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for components using the new APIs. + if ( + typeof ctor.getDerivedStateFromProps !== 'function' && + typeof instance.getSnapshotBeforeUpdate !== 'function' && + (typeof instance.UNSAFE_componentWillMount === 'function' || + typeof instance.componentWillMount === 'function') + ) { + callComponentWillMount(workInProgress, instance); + // If we had additional state updates during this life-cycle, let's + // process them now. + processUpdateQueue( + workInProgress, + newProps, + instance, + renderExpirationTime, + ); + instance.state = workInProgress.memoizedState; + } + + if (typeof instance.componentDidMount === 'function') { + workInProgress.effectTag |= Update; + } +} + +function resumeMountClassInstance( + workInProgress: Fiber, + ctor: any, + newProps: any, + renderExpirationTime: ExpirationTime, +): boolean { + const instance = workInProgress.stateNode; + + const oldProps = workInProgress.memoizedProps; + instance.props = oldProps; + + const oldContext = instance.context; + const contextType = ctor.contextType; + let nextContext = emptyContextObject; + if (typeof contextType === 'object' && contextType !== null) { + nextContext = readContext(contextType); + } else if (!disableLegacyContext) { + const nextLegacyUnmaskedContext = getUnmaskedContext( + workInProgress, + ctor, + true, + ); + nextContext = getMaskedContext(workInProgress, nextLegacyUnmaskedContext); + } + + const getDerivedStateFromProps = ctor.getDerivedStateFromProps; + const hasNewLifecycles = + typeof getDerivedStateFromProps === 'function' || + typeof instance.getSnapshotBeforeUpdate === 'function'; + + // Note: During these life-cycles, instance.props/instance.state are what + // ever the previously attempted to render - not the "current". However, + // during componentDidUpdate we pass the "current" props. + + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for components using the new APIs. + if ( + !hasNewLifecycles && + (typeof instance.UNSAFE_componentWillReceiveProps === 'function' || + typeof instance.componentWillReceiveProps === 'function') + ) { + if (oldProps !== newProps || oldContext !== nextContext) { + callComponentWillReceiveProps( + workInProgress, + instance, + newProps, + nextContext, + ); + } + } + + resetHasForceUpdateBeforeProcessing(); + + const oldState = workInProgress.memoizedState; + let newState = (instance.state = oldState); + processUpdateQueue(workInProgress, newProps, instance, renderExpirationTime); + newState = workInProgress.memoizedState; + if ( + oldProps === newProps && + oldState === newState && + !hasContextChanged() && + !checkHasForceUpdateAfterProcessing() + ) { + // If an update was already in progress, we should schedule an Update + // effect even though we're bailing out, so that cWU/cDU are called. + if (typeof instance.componentDidMount === 'function') { + workInProgress.effectTag |= Update; + } + return false; + } + + if (typeof getDerivedStateFromProps === 'function') { + applyDerivedStateFromProps( + workInProgress, + ctor, + getDerivedStateFromProps, + newProps, + ); + newState = workInProgress.memoizedState; + } + + const shouldUpdate = + checkHasForceUpdateAfterProcessing() || + checkShouldComponentUpdate( + workInProgress, + ctor, + oldProps, + newProps, + oldState, + newState, + nextContext, + ); + + if (shouldUpdate) { + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for components using the new APIs. + if ( + !hasNewLifecycles && + (typeof instance.UNSAFE_componentWillMount === 'function' || + typeof instance.componentWillMount === 'function') + ) { + if (typeof instance.componentWillMount === 'function') { + instance.componentWillMount(); + } + if (typeof instance.UNSAFE_componentWillMount === 'function') { + instance.UNSAFE_componentWillMount(); + } + } + if (typeof instance.componentDidMount === 'function') { + workInProgress.effectTag |= Update; + } + } else { + // If an update was already in progress, we should schedule an Update + // effect even though we're bailing out, so that cWU/cDU are called. + if (typeof instance.componentDidMount === 'function') { + workInProgress.effectTag |= Update; + } + + // If shouldComponentUpdate returned false, we should still update the + // memoized state to indicate that this work can be reused. + workInProgress.memoizedProps = newProps; + workInProgress.memoizedState = newState; + } + + // Update the existing instance's state, props, and context pointers even + // if shouldComponentUpdate returns false. + instance.props = newProps; + instance.state = newState; + instance.context = nextContext; + + return shouldUpdate; +} + +// Invokes the update life-cycles and returns false if it shouldn't rerender. +function updateClassInstance( + current: Fiber, + workInProgress: Fiber, + ctor: any, + newProps: any, + renderExpirationTime: ExpirationTime, +): boolean { + const instance = workInProgress.stateNode; + + cloneUpdateQueue(current, workInProgress); + + const unresolvedOldProps = workInProgress.memoizedProps; + const oldProps = + workInProgress.type === workInProgress.elementType + ? unresolvedOldProps + : resolveDefaultProps(workInProgress.type, unresolvedOldProps); + instance.props = oldProps; + const unresolvedNewProps = workInProgress.pendingProps; + + const oldContext = instance.context; + const contextType = ctor.contextType; + let nextContext = emptyContextObject; + if (typeof contextType === 'object' && contextType !== null) { + nextContext = readContext(contextType); + } else if (!disableLegacyContext) { + const nextUnmaskedContext = getUnmaskedContext(workInProgress, ctor, true); + nextContext = getMaskedContext(workInProgress, nextUnmaskedContext); + } + + const getDerivedStateFromProps = ctor.getDerivedStateFromProps; + const hasNewLifecycles = + typeof getDerivedStateFromProps === 'function' || + typeof instance.getSnapshotBeforeUpdate === 'function'; + + // Note: During these life-cycles, instance.props/instance.state are what + // ever the previously attempted to render - not the "current". However, + // during componentDidUpdate we pass the "current" props. + + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for components using the new APIs. + if ( + !hasNewLifecycles && + (typeof instance.UNSAFE_componentWillReceiveProps === 'function' || + typeof instance.componentWillReceiveProps === 'function') + ) { + if ( + unresolvedOldProps !== unresolvedNewProps || + oldContext !== nextContext + ) { + callComponentWillReceiveProps( + workInProgress, + instance, + newProps, + nextContext, + ); + } + } + + resetHasForceUpdateBeforeProcessing(); + + const oldState = workInProgress.memoizedState; + let newState = (instance.state = oldState); + processUpdateQueue(workInProgress, newProps, instance, renderExpirationTime); + newState = workInProgress.memoizedState; + + if ( + unresolvedOldProps === unresolvedNewProps && + oldState === newState && + !hasContextChanged() && + !checkHasForceUpdateAfterProcessing() + ) { + // If an update was already in progress, we should schedule an Update + // effect even though we're bailing out, so that cWU/cDU are called. + if (typeof instance.componentDidUpdate === 'function') { + if ( + unresolvedOldProps !== current.memoizedProps || + oldState !== current.memoizedState + ) { + workInProgress.effectTag |= Update; + } + } + if (typeof instance.getSnapshotBeforeUpdate === 'function') { + if ( + unresolvedOldProps !== current.memoizedProps || + oldState !== current.memoizedState + ) { + workInProgress.effectTag |= Snapshot; + } + } + return false; + } + + if (typeof getDerivedStateFromProps === 'function') { + applyDerivedStateFromProps( + workInProgress, + ctor, + getDerivedStateFromProps, + newProps, + ); + newState = workInProgress.memoizedState; + } + + const shouldUpdate = + checkHasForceUpdateAfterProcessing() || + checkShouldComponentUpdate( + workInProgress, + ctor, + oldProps, + newProps, + oldState, + newState, + nextContext, + ); + + if (shouldUpdate) { + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for components using the new APIs. + if ( + !hasNewLifecycles && + (typeof instance.UNSAFE_componentWillUpdate === 'function' || + typeof instance.componentWillUpdate === 'function') + ) { + if (typeof instance.componentWillUpdate === 'function') { + instance.componentWillUpdate(newProps, newState, nextContext); + } + if (typeof instance.UNSAFE_componentWillUpdate === 'function') { + instance.UNSAFE_componentWillUpdate(newProps, newState, nextContext); + } + } + if (typeof instance.componentDidUpdate === 'function') { + workInProgress.effectTag |= Update; + } + if (typeof instance.getSnapshotBeforeUpdate === 'function') { + workInProgress.effectTag |= Snapshot; + } + } else { + // If an update was already in progress, we should schedule an Update + // effect even though we're bailing out, so that cWU/cDU are called. + if (typeof instance.componentDidUpdate === 'function') { + if ( + unresolvedOldProps !== current.memoizedProps || + oldState !== current.memoizedState + ) { + workInProgress.effectTag |= Update; + } + } + if (typeof instance.getSnapshotBeforeUpdate === 'function') { + if ( + unresolvedOldProps !== current.memoizedProps || + oldState !== current.memoizedState + ) { + workInProgress.effectTag |= Snapshot; + } + } + + // If shouldComponentUpdate returned false, we should still update the + // memoized props/state to indicate that this work can be reused. + workInProgress.memoizedProps = newProps; + workInProgress.memoizedState = newState; + } + + // Update the existing instance's state, props, and context pointers even + // if shouldComponentUpdate returns false. + instance.props = newProps; + instance.state = newState; + instance.context = nextContext; + + return shouldUpdate; +} + +export { + adoptClassInstance, + constructClassInstance, + mountClassInstance, + resumeMountClassInstance, + updateClassInstance, +}; diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js new file mode 100644 index 0000000000000..2901e8ea35b43 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -0,0 +1,1837 @@ +/** + * 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, + SuspenseInstance, + Container, + ChildSet, + UpdatePayload, +} from './ReactFiberHostConfig'; +import type {Fiber} from './ReactInternalTypes'; +import type {FiberRoot} from './ReactInternalTypes'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; +import type {UpdateQueue} from './ReactUpdateQueue.new'; +import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new'; +import type {Wakeable} from 'shared/ReactTypes'; +import type {ReactPriorityLevel} from './ReactInternalTypes'; + +import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing'; +import { + deferPassiveEffectCleanupDuringUnmount, + enableSchedulerTracing, + enableProfilerTimer, + enableProfilerCommitHooks, + enableSuspenseServerRenderer, + enableDeprecatedFlareAPI, + enableFundamentalAPI, + enableSuspenseCallback, + enableScopeAPI, + runAllPassiveEffectDestroysBeforeCreates, + enableUseEventAPI, +} from 'shared/ReactFeatureFlags'; +import { + FunctionComponent, + ForwardRef, + ClassComponent, + HostRoot, + HostComponent, + HostText, + HostPortal, + Profiler, + SuspenseComponent, + DehydratedFragment, + IncompleteClassComponent, + MemoComponent, + SimpleMemoComponent, + SuspenseListComponent, + FundamentalComponent, + ScopeComponent, + Block, +} from './ReactWorkTags'; +import { + invokeGuardedCallback, + hasCaughtError, + clearCaughtError, +} from 'shared/ReactErrorUtils'; +import { + NoEffect, + ContentReset, + Placement, + Snapshot, + Update, + Passive, +} from './ReactSideEffectTags'; +import getComponentName from 'shared/getComponentName'; +import invariant from 'shared/invariant'; + +import {onCommitUnmount} from './ReactFiberDevToolsHook.new'; +import {getStackByFiberInDevAndProd} from './ReactFiberComponentStack'; +import {resolveDefaultProps} from './ReactFiberLazyComponent.new'; +import { + getCommitTime, + recordLayoutEffectDuration, + recordPassiveEffectDuration, + startLayoutEffectTimer, + startPassiveEffectTimer, +} from './ReactProfilerTimer.new'; +import {ProfileMode} from './ReactTypeOfMode'; +import {commitUpdateQueue} from './ReactUpdateQueue.new'; +import { + getPublicInstance, + supportsMutation, + supportsPersistence, + supportsHydration, + commitMount, + commitUpdate, + resetTextContent, + commitTextUpdate, + appendChild, + appendChildToContainer, + insertBefore, + insertInContainerBefore, + removeChild, + removeChildFromContainer, + clearSuspenseBoundary, + clearSuspenseBoundaryFromContainer, + replaceContainerChildren, + createContainerChildSet, + hideInstance, + hideTextInstance, + unhideInstance, + unhideTextInstance, + unmountFundamentalComponent, + updateFundamentalComponent, + commitHydratedContainer, + commitHydratedSuspenseInstance, + beforeRemoveInstance, +} from './ReactFiberHostConfig'; +import { + captureCommitPhaseError, + resolveRetryWakeable, + markCommitTimeOfFallback, + enqueuePendingPassiveHookEffectMount, + enqueuePendingPassiveHookEffectUnmount, + enqueuePendingPassiveProfilerEffect, +} from './ReactFiberWorkLoop.new'; +import { + NoEffect as NoHookEffect, + HasEffect as HookHasEffect, + Layout as HookLayout, + Passive as HookPassive, +} from './ReactHookEffectTags'; +import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new'; +import { + runWithPriority, + NormalPriority, +} from './SchedulerWithReactIntegration.new'; +import { + updateDeprecatedEventListeners, + unmountDeprecatedResponderListeners, +} from './ReactFiberDeprecatedEvents.new'; + +let didWarnAboutUndefinedSnapshotBeforeUpdate: Set | null = null; +if (__DEV__) { + didWarnAboutUndefinedSnapshotBeforeUpdate = new Set(); +} + +const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set; + +const callComponentWillUnmountWithTimer = function(current, instance) { + instance.props = current.memoizedProps; + instance.state = current.memoizedState; + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + current.mode & ProfileMode + ) { + try { + startLayoutEffectTimer(); + instance.componentWillUnmount(); + } finally { + recordLayoutEffectDuration(current); + } + } else { + instance.componentWillUnmount(); + } +}; + +// 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 safelyCallDestroy(current, destroy) { + if (__DEV__) { + invokeGuardedCallback(null, destroy, null); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(current, error); + } + } else { + try { + destroy(); + } catch (error) { + captureCommitPhaseError(current, error); + } + } +} + +function commitBeforeMutationLifeCycles( + current: Fiber | null, + finishedWork: Fiber, +): void { + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: + case Block: { + return; + } + case ClassComponent: { + if (finishedWork.effectTag & Snapshot) { + if (current !== null) { + const prevProps = current.memoizedProps; + const prevState = current.memoizedState; + const instance = finishedWork.stateNode; + // We could update instance props and state here, + // but instead we rely on them being set during last render. + // TODO: revisit this when we implement resuming. + if (__DEV__) { + if ( + finishedWork.type === finishedWork.elementType && + !didWarnAboutReassigningProps + ) { + if (instance.props !== finishedWork.memoizedProps) { + console.error( + 'Expected %s props to match memoized props before ' + + 'getSnapshotBeforeUpdate. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.props`. ' + + 'Please file an issue.', + getComponentName(finishedWork.type) || 'instance', + ); + } + if (instance.state !== finishedWork.memoizedState) { + console.error( + 'Expected %s state to match memoized state before ' + + 'getSnapshotBeforeUpdate. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.state`. ' + + 'Please file an issue.', + getComponentName(finishedWork.type) || 'instance', + ); + } + } + } + const snapshot = instance.getSnapshotBeforeUpdate( + finishedWork.elementType === finishedWork.type + ? prevProps + : resolveDefaultProps(finishedWork.type, prevProps), + prevState, + ); + if (__DEV__) { + const didWarnSet = ((didWarnAboutUndefinedSnapshotBeforeUpdate: any): Set); + if (snapshot === undefined && !didWarnSet.has(finishedWork.type)) { + didWarnSet.add(finishedWork.type); + console.error( + '%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' + + 'must be returned. You have returned undefined.', + getComponentName(finishedWork.type), + ); + } + } + instance.__reactInternalSnapshotBeforeUpdate = snapshot; + } + } + return; + } + case HostRoot: + case HostComponent: + case HostText: + case HostPortal: + case IncompleteClassComponent: + // Nothing to do for these component types + return; + } + 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 commitHookEffectListUnmount(tag: number, finishedWork: Fiber) { + const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); + const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; + if (lastEffect !== null) { + const firstEffect = lastEffect.next; + let effect = firstEffect; + do { + if ((effect.tag & tag) === tag) { + // Unmount + const destroy = effect.destroy; + effect.destroy = undefined; + if (destroy !== undefined) { + destroy(); + } + } + effect = effect.next; + } while (effect !== firstEffect); + } +} + +function commitHookEffectListMount(tag: number, finishedWork: Fiber) { + const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); + const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; + if (lastEffect !== null) { + const firstEffect = lastEffect.next; + let effect = firstEffect; + do { + if ((effect.tag & tag) === tag) { + // Mount + const create = effect.create; + effect.destroy = create(); + + if (__DEV__) { + const destroy = effect.destroy; + if (destroy !== undefined && typeof destroy !== 'function') { + let addendum; + if (destroy === null) { + addendum = + ' You returned null. If your effect does not require clean ' + + 'up, return undefined (or nothing).'; + } else if (typeof destroy.then === 'function') { + addendum = + '\n\nIt looks like you wrote useEffect(async () => ...) or returned a Promise. ' + + 'Instead, write the async function inside your effect ' + + 'and call it immediately:\n\n' + + 'useEffect(() => {\n' + + ' async function fetchData() {\n' + + ' // You can await here\n' + + ' const response = await MyAPI.getData(someId);\n' + + ' // ...\n' + + ' }\n' + + ' fetchData();\n' + + `}, [someId]); // Or [] if effect doesn't need props or state\n\n` + + 'Learn more about data fetching with Hooks: https://fb.me/react-hooks-data-fetching'; + } else { + addendum = ' You returned: ' + destroy; + } + console.error( + 'An effect function must not return anything besides a function, ' + + 'which is used for clean-up.%s%s', + addendum, + getStackByFiberInDevAndProd(finishedWork), + ); + } + } + } + effect = effect.next; + } while (effect !== firstEffect); + } +} + +function schedulePassiveEffects(finishedWork: Fiber) { + if (runAllPassiveEffectDestroysBeforeCreates) { + const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); + const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; + if (lastEffect !== null) { + const firstEffect = lastEffect.next; + let effect = firstEffect; + do { + const {next, tag} = effect; + if ( + (tag & HookPassive) !== NoHookEffect && + (tag & HookHasEffect) !== NoHookEffect + ) { + enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); + enqueuePendingPassiveHookEffectMount(finishedWork, effect); + } + effect = next; + } while (effect !== firstEffect); + } + } +} + +export function commitPassiveHookEffects(finishedWork: Fiber): void { + if ((finishedWork.effectTag & Passive) !== NoEffect) { + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: + case Block: { + // TODO (#17945) We should call all passive destroy functions (for all fibers) + // before calling any create functions. The current approach only serializes + // these for a single fiber. + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.mode & ProfileMode + ) { + try { + startPassiveEffectTimer(); + commitHookEffectListUnmount( + HookPassive | HookHasEffect, + finishedWork, + ); + commitHookEffectListMount( + HookPassive | HookHasEffect, + finishedWork, + ); + } finally { + recordPassiveEffectDuration(finishedWork); + } + } else { + commitHookEffectListUnmount( + HookPassive | HookHasEffect, + finishedWork, + ); + commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork); + } + break; + } + default: + break; + } + } +} + +export function commitPassiveEffectDurations( + finishedRoot: FiberRoot, + finishedWork: Fiber, +): void { + if (enableProfilerTimer && enableProfilerCommitHooks) { + // Only Profilers with work in their subtree will have an Update effect scheduled. + if ((finishedWork.effectTag & Update) !== NoEffect) { + switch (finishedWork.tag) { + case Profiler: { + const {passiveEffectDuration} = finishedWork.stateNode; + const {id, onPostCommit} = finishedWork.memoizedProps; + + // This value will still reflect the previous commit phase. + // It does not get reset until the start of the next commit phase. + const commitTime = getCommitTime(); + + if (typeof onPostCommit === 'function') { + if (enableSchedulerTracing) { + onPostCommit( + id, + finishedWork.alternate === null ? 'mount' : 'update', + passiveEffectDuration, + commitTime, + finishedRoot.memoizedInteractions, + ); + } else { + onPostCommit( + id, + finishedWork.alternate === null ? 'mount' : 'update', + passiveEffectDuration, + commitTime, + ); + } + } + + // Bubble times to the next nearest ancestor Profiler. + // After we process that Profiler, we'll bubble further up. + let parentFiber = finishedWork.return; + while (parentFiber !== null) { + if (parentFiber.tag === Profiler) { + const parentStateNode = parentFiber.stateNode; + parentStateNode.passiveEffectDuration += passiveEffectDuration; + break; + } + parentFiber = parentFiber.return; + } + break; + } + default: + break; + } + } + } +} + +function commitLifeCycles( + finishedRoot: FiberRoot, + current: Fiber | null, + finishedWork: Fiber, + committedExpirationTime: ExpirationTime, +): void { + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: + case Block: { + // At this point layout effects have already been destroyed (during mutation phase). + // This is done to prevent sibling component effects from interfering with each other, + // e.g. a destroy function in one component should never override a ref set + // by a create function in another component during the same commit. + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.mode & ProfileMode + ) { + try { + startLayoutEffectTimer(); + commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); + } finally { + recordLayoutEffectDuration(finishedWork); + } + } else { + commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); + } + + if (runAllPassiveEffectDestroysBeforeCreates) { + schedulePassiveEffects(finishedWork); + } + return; + } + case ClassComponent: { + const instance = finishedWork.stateNode; + if (finishedWork.effectTag & Update) { + if (current === null) { + // We could update instance props and state here, + // but instead we rely on them being set during last render. + // TODO: revisit this when we implement resuming. + if (__DEV__) { + if ( + finishedWork.type === finishedWork.elementType && + !didWarnAboutReassigningProps + ) { + if (instance.props !== finishedWork.memoizedProps) { + console.error( + 'Expected %s props to match memoized props before ' + + 'componentDidMount. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.props`. ' + + 'Please file an issue.', + getComponentName(finishedWork.type) || 'instance', + ); + } + if (instance.state !== finishedWork.memoizedState) { + console.error( + 'Expected %s state to match memoized state before ' + + 'componentDidMount. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.state`. ' + + 'Please file an issue.', + getComponentName(finishedWork.type) || 'instance', + ); + } + } + } + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.mode & ProfileMode + ) { + try { + startLayoutEffectTimer(); + instance.componentDidMount(); + } finally { + recordLayoutEffectDuration(finishedWork); + } + } else { + instance.componentDidMount(); + } + } else { + const prevProps = + finishedWork.elementType === finishedWork.type + ? current.memoizedProps + : resolveDefaultProps(finishedWork.type, current.memoizedProps); + const prevState = current.memoizedState; + // We could update instance props and state here, + // but instead we rely on them being set during last render. + // TODO: revisit this when we implement resuming. + if (__DEV__) { + if ( + finishedWork.type === finishedWork.elementType && + !didWarnAboutReassigningProps + ) { + if (instance.props !== finishedWork.memoizedProps) { + console.error( + 'Expected %s props to match memoized props before ' + + 'componentDidUpdate. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.props`. ' + + 'Please file an issue.', + getComponentName(finishedWork.type) || 'instance', + ); + } + if (instance.state !== finishedWork.memoizedState) { + console.error( + 'Expected %s state to match memoized state before ' + + 'componentDidUpdate. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.state`. ' + + 'Please file an issue.', + getComponentName(finishedWork.type) || 'instance', + ); + } + } + } + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.mode & ProfileMode + ) { + try { + startLayoutEffectTimer(); + instance.componentDidUpdate( + prevProps, + prevState, + instance.__reactInternalSnapshotBeforeUpdate, + ); + } finally { + recordLayoutEffectDuration(finishedWork); + } + } else { + instance.componentDidUpdate( + prevProps, + prevState, + instance.__reactInternalSnapshotBeforeUpdate, + ); + } + } + } + + // TODO: I think this is now always non-null by the time it reaches the + // commit phase. Consider removing the type check. + const updateQueue: UpdateQueue< + *, + > | null = (finishedWork.updateQueue: any); + if (updateQueue !== null) { + if (__DEV__) { + if ( + finishedWork.type === finishedWork.elementType && + !didWarnAboutReassigningProps + ) { + if (instance.props !== finishedWork.memoizedProps) { + console.error( + 'Expected %s props to match memoized props before ' + + 'processing the update queue. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.props`. ' + + 'Please file an issue.', + getComponentName(finishedWork.type) || 'instance', + ); + } + if (instance.state !== finishedWork.memoizedState) { + console.error( + 'Expected %s state to match memoized state before ' + + 'processing the update queue. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.state`. ' + + 'Please file an issue.', + getComponentName(finishedWork.type) || 'instance', + ); + } + } + } + // We could update instance props and state here, + // but instead we rely on them being set during last render. + // TODO: revisit this when we implement resuming. + commitUpdateQueue(finishedWork, updateQueue, instance); + } + return; + } + case HostRoot: { + // TODO: I think this is now always non-null by the time it reaches the + // commit phase. Consider removing the type check. + const updateQueue: UpdateQueue< + *, + > | null = (finishedWork.updateQueue: any); + if (updateQueue !== null) { + let instance = null; + if (finishedWork.child !== null) { + switch (finishedWork.child.tag) { + case HostComponent: + instance = getPublicInstance(finishedWork.child.stateNode); + break; + case ClassComponent: + instance = finishedWork.child.stateNode; + break; + } + } + commitUpdateQueue(finishedWork, updateQueue, instance); + } + 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 {onCommit, onRender} = finishedWork.memoizedProps; + const {effectDuration} = finishedWork.stateNode; + + const commitTime = getCommitTime(); + + if (typeof onRender === 'function') { + if (enableSchedulerTracing) { + onRender( + finishedWork.memoizedProps.id, + current === null ? 'mount' : 'update', + finishedWork.actualDuration, + finishedWork.treeBaseDuration, + finishedWork.actualStartTime, + commitTime, + finishedRoot.memoizedInteractions, + ); + } else { + onRender( + finishedWork.memoizedProps.id, + current === null ? 'mount' : 'update', + finishedWork.actualDuration, + finishedWork.treeBaseDuration, + finishedWork.actualStartTime, + commitTime, + ); + } + } + + if (enableProfilerCommitHooks) { + if (typeof onCommit === 'function') { + if (enableSchedulerTracing) { + onCommit( + finishedWork.memoizedProps.id, + current === null ? 'mount' : 'update', + effectDuration, + commitTime, + finishedRoot.memoizedInteractions, + ); + } else { + onCommit( + finishedWork.memoizedProps.id, + current === null ? 'mount' : 'update', + effectDuration, + commitTime, + ); + } + } + + // Schedule a passive effect for this Profiler to call onPostCommit hooks. + // This effect should be scheduled even if there is no onPostCommit callback for this Profiler, + // because the effect is also where times bubble to parent Profilers. + enqueuePendingPassiveProfilerEffect(finishedWork); + + // Propagate layout effect durations to the next nearest Profiler ancestor. + // Do not reset these values until the next render so DevTools has a chance to read them first. + let parentFiber = finishedWork.return; + while (parentFiber !== null) { + if (parentFiber.tag === Profiler) { + const parentStateNode = parentFiber.stateNode; + parentStateNode.effectDuration += effectDuration; + break; + } + parentFiber = parentFiber.return; + } + } + } + return; + } + case SuspenseComponent: { + commitSuspenseHydrationCallbacks(finishedRoot, finishedWork); + return; + } + case SuspenseListComponent: + case IncompleteClassComponent: + case FundamentalComponent: + case ScopeComponent: + return; + } + 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 hideOrUnhideAllChildren(finishedWork, isHidden) { + if (supportsMutation) { + // We only have the top Fiber that was inserted but we need to recurse down its + // children to find all the terminal nodes. + let node: Fiber = finishedWork; + while (true) { + if (node.tag === HostComponent) { + const instance = node.stateNode; + if (isHidden) { + hideInstance(instance); + } else { + unhideInstance(node.stateNode, node.memoizedProps); + } + } else if (node.tag === HostText) { + const instance = node.stateNode; + if (isHidden) { + hideTextInstance(instance); + } else { + unhideTextInstance(instance, node.memoizedProps); + } + } else if ( + node.tag === SuspenseComponent && + node.memoizedState !== null && + node.memoizedState.dehydrated === null + ) { + // Found a nested Suspense component that timed out. Skip over the + // primary child fragment, which should remain hidden. + const fallbackChildFragment: Fiber = (node.child: any).sibling; + fallbackChildFragment.return = node; + node = fallbackChildFragment; + continue; + } 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 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; + } + // Moved outside to ensure DCE works with this flag + if (enableScopeAPI && finishedWork.tag === ScopeComponent) { + instanceToUse = instance.methods; + } + if (typeof ref === 'function') { + ref(instanceToUse); + } else { + if (__DEV__) { + if (!ref.hasOwnProperty('current')) { + console.error( + '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( + finishedRoot: FiberRoot, + current: Fiber, + renderPriorityLevel: ReactPriorityLevel, +): void { + onCommitUnmount(current); + + switch (current.tag) { + case FunctionComponent: + case ForwardRef: + case MemoComponent: + case SimpleMemoComponent: + case Block: { + const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any); + if (updateQueue !== null) { + const lastEffect = updateQueue.lastEffect; + if (lastEffect !== null) { + const firstEffect = lastEffect.next; + + if ( + deferPassiveEffectCleanupDuringUnmount && + runAllPassiveEffectDestroysBeforeCreates + ) { + let effect = firstEffect; + do { + const {destroy, tag} = effect; + if (destroy !== undefined) { + if ((tag & HookPassive) !== NoHookEffect) { + enqueuePendingPassiveHookEffectUnmount(current, effect); + } else { + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + current.mode & ProfileMode + ) { + startLayoutEffectTimer(); + safelyCallDestroy(current, destroy); + recordLayoutEffectDuration(current); + } else { + safelyCallDestroy(current, destroy); + } + } + } + effect = effect.next; + } while (effect !== firstEffect); + } else { + // When the owner fiber is deleted, the destroy function of a passive + // effect hook is called during the synchronous commit phase. This is + // a concession to implementation complexity. Calling it in the + // passive effect phase (like they usually are, when dependencies + // change during an update) would require either traversing the + // children of the deleted fiber again, or including unmount effects + // as part of the fiber effect list. + // + // Because this is during the sync commit phase, we need to change + // the priority. + // + // TODO: Reconsider this implementation trade off. + const priorityLevel = + renderPriorityLevel > NormalPriority + ? NormalPriority + : renderPriorityLevel; + runWithPriority(priorityLevel, () => { + let effect = firstEffect; + do { + const {destroy, tag} = effect; + if (destroy !== undefined) { + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + current.mode & ProfileMode + ) { + if ((tag & HookPassive) !== NoHookEffect) { + safelyCallDestroy(current, destroy); + } else { + startLayoutEffectTimer(); + safelyCallDestroy(current, destroy); + recordLayoutEffectDuration(current); + } + } else { + safelyCallDestroy(current, destroy); + } + } + effect = effect.next; + } while (effect !== firstEffect); + }); + } + } + } + return; + } + case ClassComponent: { + safelyDetachRef(current); + const instance = current.stateNode; + if (typeof instance.componentWillUnmount === 'function') { + safelyCallComponentWillUnmount(current, instance); + } + return; + } + case HostComponent: { + if (enableDeprecatedFlareAPI) { + unmountDeprecatedResponderListeners(current); + } + if (enableDeprecatedFlareAPI || enableUseEventAPI) { + beforeRemoveInstance(current.stateNode); + } + 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(finishedRoot, current, renderPriorityLevel); + } else if (supportsPersistence) { + emptyPortalContainer(current); + } + return; + } + case FundamentalComponent: { + if (enableFundamentalAPI) { + const fundamentalInstance = current.stateNode; + if (fundamentalInstance !== null) { + unmountFundamentalComponent(fundamentalInstance); + current.stateNode = null; + } + } + return; + } + case DehydratedFragment: { + if (enableSuspenseCallback) { + const hydrationCallbacks = finishedRoot.hydrationCallbacks; + if (hydrationCallbacks !== null) { + const onDeleted = hydrationCallbacks.onDeleted; + if (onDeleted) { + onDeleted((current.stateNode: SuspenseInstance)); + } + } + } + return; + } + case ScopeComponent: { + if (enableDeprecatedFlareAPI) { + unmountDeprecatedResponderListeners(current); + } + if (enableScopeAPI) { + safelyDetachRef(current); + } + return; + } + } +} + +function commitNestedUnmounts( + finishedRoot: FiberRoot, + root: Fiber, + renderPriorityLevel: ReactPriorityLevel, +): 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(finishedRoot, node, renderPriorityLevel); + // 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(fiber: 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. + fiber.return = null; + fiber.child = null; + fiber.memoizedState = null; + fiber.updateQueue = null; + fiber.dependencies = null; + fiber.alternate = null; + fiber.firstEffect = null; + fiber.lastEffect = null; + fiber.pendingProps = null; + fiber.memoizedProps = null; + fiber.stateNode = 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 HostComponent: + case HostText: + case FundamentalComponent: { + return; + } + case HostRoot: + case HostPortal: { + const portalOrRoot: { + containerInfo: Container, + pendingChildren: ChildSet, + ... + } = finishedWork.stateNode; + const {containerInfo, pendingChildren} = portalOrRoot; + replaceContainerChildren(containerInfo, pendingChildren); + return; + } + } + 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 && + node.tag !== DehydratedFragment + ) { + // 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; + const parentStateNode = parentFiber.stateNode; + switch (parentFiber.tag) { + case HostComponent: + parent = parentStateNode; + isContainer = false; + break; + case HostRoot: + parent = parentStateNode.containerInfo; + isContainer = true; + break; + case HostPortal: + parent = parentStateNode.containerInfo; + isContainer = true; + break; + case FundamentalComponent: + if (enableFundamentalAPI) { + parent = parentStateNode.instance; + isContainer = false; + } + // eslint-disable-next-line-no-fallthrough + 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 to recurse down its + // children to find all the terminal nodes. + if (isContainer) { + insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent); + } else { + insertOrAppendPlacementNode(finishedWork, before, parent); + } +} + +function insertOrAppendPlacementNodeIntoContainer( + node: Fiber, + before: ?Instance, + parent: Container, +): void { + const {tag} = node; + const isHost = tag === HostComponent || tag === HostText; + if (isHost || (enableFundamentalAPI && tag === FundamentalComponent)) { + const stateNode = isHost ? node.stateNode : node.stateNode.instance; + if (before) { + insertInContainerBefore(parent, stateNode, before); + } else { + appendChildToContainer(parent, stateNode); + } + } else if (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 { + const child = node.child; + if (child !== null) { + insertOrAppendPlacementNodeIntoContainer(child, before, parent); + let sibling = child.sibling; + while (sibling !== null) { + insertOrAppendPlacementNodeIntoContainer(sibling, before, parent); + sibling = sibling.sibling; + } + } + } +} + +function insertOrAppendPlacementNode( + node: Fiber, + before: ?Instance, + parent: Instance, +): void { + const {tag} = node; + const isHost = tag === HostComponent || tag === HostText; + if (isHost || (enableFundamentalAPI && tag === FundamentalComponent)) { + const stateNode = isHost ? node.stateNode : node.stateNode.instance; + if (before) { + insertBefore(parent, stateNode, before); + } else { + appendChild(parent, stateNode); + } + } else if (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 { + const child = node.child; + if (child !== null) { + insertOrAppendPlacementNode(child, before, parent); + let sibling = child.sibling; + while (sibling !== null) { + insertOrAppendPlacementNode(sibling, before, parent); + sibling = sibling.sibling; + } + } + } +} + +function unmountHostComponents( + finishedRoot, + current, + renderPriorityLevel, +): void { + // We only have the top Fiber that was deleted but we need to 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.', + ); + const parentStateNode = parent.stateNode; + switch (parent.tag) { + case HostComponent: + currentParent = parentStateNode; + currentParentIsContainer = false; + break findParent; + case HostRoot: + currentParent = parentStateNode.containerInfo; + currentParentIsContainer = true; + break findParent; + case HostPortal: + currentParent = parentStateNode.containerInfo; + currentParentIsContainer = true; + break findParent; + case FundamentalComponent: + if (enableFundamentalAPI) { + currentParent = parentStateNode.instance; + currentParentIsContainer = false; + } + } + parent = parent.return; + } + currentParentIsValid = true; + } + + if (node.tag === HostComponent || node.tag === HostText) { + commitNestedUnmounts(finishedRoot, node, renderPriorityLevel); + // After all the children have unmounted, it is now safe to remove the + // node from the tree. + if (currentParentIsContainer) { + removeChildFromContainer( + ((currentParent: any): Container), + (node.stateNode: Instance | TextInstance), + ); + } else { + removeChild( + ((currentParent: any): Instance), + (node.stateNode: Instance | TextInstance), + ); + } + // Don't visit children because we already visited them. + } else if (enableFundamentalAPI && node.tag === FundamentalComponent) { + const fundamentalNode = node.stateNode.instance; + commitNestedUnmounts(finishedRoot, node, renderPriorityLevel); + // After all the children have unmounted, it is now safe to remove the + // node from the tree. + if (currentParentIsContainer) { + removeChildFromContainer( + ((currentParent: any): Container), + (fundamentalNode: Instance), + ); + } else { + removeChild( + ((currentParent: any): Instance), + (fundamentalNode: Instance), + ); + } + } else if ( + enableSuspenseServerRenderer && + node.tag === DehydratedFragment + ) { + if (enableSuspenseCallback) { + const hydrationCallbacks = finishedRoot.hydrationCallbacks; + if (hydrationCallbacks !== null) { + const onDeleted = hydrationCallbacks.onDeleted; + if (onDeleted) { + onDeleted((node.stateNode: SuspenseInstance)); + } + } + } + + // Delete the dehydrated suspense boundary and all of its content. + if (currentParentIsContainer) { + clearSuspenseBoundaryFromContainer( + ((currentParent: any): Container), + (node.stateNode: SuspenseInstance), + ); + } else { + clearSuspenseBoundary( + ((currentParent: any): Instance), + (node.stateNode: SuspenseInstance), + ); + } + } else if (node.tag === HostPortal) { + if (node.child !== null) { + // 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. + node.child.return = node; + node = node.child; + continue; + } + } else { + commitUnmount(finishedRoot, node, renderPriorityLevel); + // 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( + finishedRoot: FiberRoot, + current: Fiber, + renderPriorityLevel: ReactPriorityLevel, +): void { + if (supportsMutation) { + // Recursively delete all host nodes from the parent. + // Detach refs and call componentWillUnmount() on the whole subtree. + unmountHostComponents(finishedRoot, current, renderPriorityLevel); + } else { + // Detach refs and call componentWillUnmount() on the whole subtree. + commitNestedUnmounts(finishedRoot, current, renderPriorityLevel); + } + const alternate = current.alternate; + detachFiber(current); + if (alternate !== null) { + detachFiber(alternate); + } +} + +function commitWork(current: Fiber | null, finishedWork: Fiber): void { + if (!supportsMutation) { + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case MemoComponent: + case SimpleMemoComponent: + case Block: { + // Layout effects are destroyed during the mutation phase so that all + // destroy functions for all fibers are called before any create functions. + // This prevents sibling component effects from interfering with each other, + // e.g. a destroy function in one component should never override a ref set + // by a create function in another component during the same commit. + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.mode & ProfileMode + ) { + try { + startLayoutEffectTimer(); + commitHookEffectListUnmount( + HookLayout | HookHasEffect, + finishedWork, + ); + } finally { + recordLayoutEffectDuration(finishedWork); + } + } else { + commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork); + } + return; + } + case Profiler: { + return; + } + case SuspenseComponent: { + commitSuspenseComponent(finishedWork); + attachSuspenseRetryListeners(finishedWork); + return; + } + case SuspenseListComponent: { + attachSuspenseRetryListeners(finishedWork); + return; + } + case HostRoot: { + if (supportsHydration) { + const root: FiberRoot = finishedWork.stateNode; + if (root.hydrate) { + // We've just hydrated. No need to hydrate again. + root.hydrate = false; + commitHydratedContainer(root.containerInfo); + } + } + break; + } + } + + commitContainer(finishedWork); + return; + } + + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case MemoComponent: + case SimpleMemoComponent: + case Block: { + // Layout effects are destroyed during the mutation phase so that all + // destroy functions for all fibers are called before any create functions. + // This prevents sibling component effects from interfering with each other, + // e.g. a destroy function in one component should never override a ref set + // by a create function in another component during the same commit. + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.mode & ProfileMode + ) { + try { + startLayoutEffectTimer(); + commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork); + } finally { + recordLayoutEffectDuration(finishedWork); + } + } else { + commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork); + } + return; + } + case ClassComponent: { + 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, + ); + } + if (enableDeprecatedFlareAPI) { + const prevListeners = oldProps.DEPRECATED_flareListeners; + const nextListeners = newProps.DEPRECATED_flareListeners; + if (prevListeners !== nextListeners) { + updateDeprecatedEventListeners(nextListeners, finishedWork, null); + } + } + } + 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: { + if (supportsHydration) { + const root: FiberRoot = finishedWork.stateNode; + if (root.hydrate) { + // We've just hydrated. No need to hydrate again. + root.hydrate = false; + commitHydratedContainer(root.containerInfo); + } + } + return; + } + case Profiler: { + return; + } + case SuspenseComponent: { + commitSuspenseComponent(finishedWork); + attachSuspenseRetryListeners(finishedWork); + return; + } + case SuspenseListComponent: { + attachSuspenseRetryListeners(finishedWork); + return; + } + case IncompleteClassComponent: { + return; + } + case FundamentalComponent: { + if (enableFundamentalAPI) { + const fundamentalInstance = finishedWork.stateNode; + updateFundamentalComponent(fundamentalInstance); + return; + } + break; + } + case ScopeComponent: { + if (enableScopeAPI) { + const scopeInstance = finishedWork.stateNode; + scopeInstance.fiber = finishedWork; + if (enableDeprecatedFlareAPI) { + const newProps = finishedWork.memoizedProps; + const oldProps = current !== null ? current.memoizedProps : newProps; + const prevListeners = oldProps.DEPRECATED_flareListeners; + const nextListeners = newProps.DEPRECATED_flareListeners; + if (prevListeners !== nextListeners || current === null) { + updateDeprecatedEventListeners(nextListeners, finishedWork, null); + } + } + return; + } + break; + } + } + 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 commitSuspenseComponent(finishedWork: Fiber) { + const newState: SuspenseState | null = finishedWork.memoizedState; + + let newDidTimeout; + let primaryChildParent = finishedWork; + if (newState === null) { + newDidTimeout = false; + } else { + newDidTimeout = true; + primaryChildParent = finishedWork.child; + markCommitTimeOfFallback(); + } + + if (supportsMutation && primaryChildParent !== null) { + hideOrUnhideAllChildren(primaryChildParent, newDidTimeout); + } + + if (enableSuspenseCallback && newState !== null) { + const suspenseCallback = finishedWork.memoizedProps.suspenseCallback; + if (typeof suspenseCallback === 'function') { + const wakeables: Set | null = (finishedWork.updateQueue: any); + if (wakeables !== null) { + suspenseCallback(new Set(wakeables)); + } + } else if (__DEV__) { + if (suspenseCallback !== undefined) { + console.error('Unexpected type for suspenseCallback.'); + } + } + } +} + +function commitSuspenseHydrationCallbacks( + finishedRoot: FiberRoot, + finishedWork: Fiber, +) { + if (!supportsHydration) { + return; + } + const newState: SuspenseState | null = finishedWork.memoizedState; + if (newState === null) { + const current = finishedWork.alternate; + if (current !== null) { + const prevState: SuspenseState | null = current.memoizedState; + if (prevState !== null) { + const suspenseInstance = prevState.dehydrated; + if (suspenseInstance !== null) { + commitHydratedSuspenseInstance(suspenseInstance); + if (enableSuspenseCallback) { + const hydrationCallbacks = finishedRoot.hydrationCallbacks; + if (hydrationCallbacks !== null) { + const onHydrated = hydrationCallbacks.onHydrated; + if (onHydrated) { + onHydrated(suspenseInstance); + } + } + } + } + } + } + } +} + +function attachSuspenseRetryListeners(finishedWork: Fiber) { + // If this boundary just timed out, then it will have a set of wakeables. + // For each wakeable, attach a listener so that when it resolves, React + // attempts to re-render the boundary in the primary (pre-timeout) state. + const wakeables: Set | null = (finishedWork.updateQueue: any); + if (wakeables !== null) { + finishedWork.updateQueue = null; + let retryCache = finishedWork.stateNode; + if (retryCache === null) { + retryCache = finishedWork.stateNode = new PossiblyWeakSet(); + } + wakeables.forEach(wakeable => { + // Memoize using the boundary fiber to prevent redundant listeners. + let retry = resolveRetryWakeable.bind(null, finishedWork, wakeable); + if (!retryCache.has(wakeable)) { + if (enableSchedulerTracing) { + if (wakeable.__reactDoNotTraceInteractions !== true) { + retry = Schedule_tracing_wrap(retry); + } + } + retryCache.add(wakeable); + wakeable.then(retry, retry); + } + }); + } +} + +function commitResetTextContent(current: Fiber) { + if (!supportsMutation) { + return; + } + resetTextContent(current.stateNode); +} + +export { + commitBeforeMutationLifeCycles, + commitResetTextContent, + commitPlacement, + commitDeletion, + commitWork, + commitLifeCycles, + commitAttachRef, + commitDetachRef, +}; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js new file mode 100644 index 0000000000000..b4ef64b2d9aed --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -0,0 +1,1314 @@ +/** + * 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'; +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'; +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}; diff --git a/packages/react-reconciler/src/ReactFiberContext.new.js b/packages/react-reconciler/src/ReactFiberContext.new.js new file mode 100644 index 0000000000000..e96295be857a8 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberContext.new.js @@ -0,0 +1,338 @@ +/** + * 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 {StackCursor} from './ReactFiberStack.new'; + +import {isFiberMounted} from './ReactFiberTreeReflection'; +import {disableLegacyContext} from 'shared/ReactFeatureFlags'; +import {ClassComponent, HostRoot} from './ReactWorkTags'; +import getComponentName from 'shared/getComponentName'; +import invariant from 'shared/invariant'; +import checkPropTypes from 'shared/checkPropTypes'; + +import {createCursor, push, pop} from './ReactFiberStack.new'; + +let warnedAboutMissingGetChildContext; + +if (__DEV__) { + warnedAboutMissingGetChildContext = {}; +} + +export const emptyContextObject = {}; +if (__DEV__) { + Object.freeze(emptyContextObject); +} + +// A cursor to the current merged context object on the stack. +const contextStackCursor: StackCursor = createCursor( + emptyContextObject, +); +// A cursor to a boolean indicating whether the context has changed. +const didPerformWorkStackCursor: StackCursor = createCursor(false); +// Keep track of the previous context object that was on the stack. +// We use this to get access to the parent context after we have already +// pushed the next context provider, and now need to merge their contexts. +let previousContext: Object = emptyContextObject; + +function getUnmaskedContext( + workInProgress: Fiber, + Component: Function, + didPushOwnContextIfProvider: boolean, +): Object { + if (disableLegacyContext) { + return emptyContextObject; + } else { + if (didPushOwnContextIfProvider && isContextProvider(Component)) { + // If the fiber is a context provider itself, when we read its context + // we may have already pushed its own child context on the stack. A context + // provider should not "see" its own child context. Therefore we read the + // previous (parent) context instead for a context provider. + return previousContext; + } + return contextStackCursor.current; + } +} + +function cacheContext( + workInProgress: Fiber, + unmaskedContext: Object, + maskedContext: Object, +): void { + if (disableLegacyContext) { + return; + } else { + const instance = workInProgress.stateNode; + instance.__reactInternalMemoizedUnmaskedChildContext = unmaskedContext; + instance.__reactInternalMemoizedMaskedChildContext = maskedContext; + } +} + +function getMaskedContext( + workInProgress: Fiber, + unmaskedContext: Object, +): Object { + if (disableLegacyContext) { + return emptyContextObject; + } else { + const type = workInProgress.type; + const contextTypes = type.contextTypes; + if (!contextTypes) { + return emptyContextObject; + } + + // Avoid recreating masked context unless unmasked context has changed. + // Failing to do this will result in unnecessary calls to componentWillReceiveProps. + // This may trigger infinite loops if componentWillReceiveProps calls setState. + const instance = workInProgress.stateNode; + if ( + instance && + instance.__reactInternalMemoizedUnmaskedChildContext === unmaskedContext + ) { + return instance.__reactInternalMemoizedMaskedChildContext; + } + + const context = {}; + for (const key in contextTypes) { + context[key] = unmaskedContext[key]; + } + + if (__DEV__) { + const name = getComponentName(type) || 'Unknown'; + checkPropTypes(contextTypes, context, 'context', name); + } + + // Cache unmasked context so we can avoid recreating masked context unless necessary. + // Context is created before the class component is instantiated so check for instance. + if (instance) { + cacheContext(workInProgress, unmaskedContext, context); + } + + return context; + } +} + +function hasContextChanged(): boolean { + if (disableLegacyContext) { + return false; + } else { + return didPerformWorkStackCursor.current; + } +} + +function isContextProvider(type: Function): boolean { + if (disableLegacyContext) { + return false; + } else { + const childContextTypes = type.childContextTypes; + return childContextTypes !== null && childContextTypes !== undefined; + } +} + +function popContext(fiber: Fiber): void { + if (disableLegacyContext) { + return; + } else { + pop(didPerformWorkStackCursor, fiber); + pop(contextStackCursor, fiber); + } +} + +function popTopLevelContextObject(fiber: Fiber): void { + if (disableLegacyContext) { + return; + } else { + pop(didPerformWorkStackCursor, fiber); + pop(contextStackCursor, fiber); + } +} + +function pushTopLevelContextObject( + fiber: Fiber, + context: Object, + didChange: boolean, +): void { + if (disableLegacyContext) { + return; + } else { + invariant( + contextStackCursor.current === emptyContextObject, + 'Unexpected context found on stack. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + + push(contextStackCursor, context, fiber); + push(didPerformWorkStackCursor, didChange, fiber); + } +} + +function processChildContext( + fiber: Fiber, + type: any, + parentContext: Object, +): Object { + if (disableLegacyContext) { + return parentContext; + } else { + const instance = fiber.stateNode; + const childContextTypes = type.childContextTypes; + + // TODO (bvaughn) Replace this behavior with an invariant() in the future. + // It has only been added in Fiber to match the (unintentional) behavior in Stack. + if (typeof instance.getChildContext !== 'function') { + if (__DEV__) { + const componentName = getComponentName(type) || 'Unknown'; + + if (!warnedAboutMissingGetChildContext[componentName]) { + warnedAboutMissingGetChildContext[componentName] = true; + console.error( + '%s.childContextTypes is specified but there is no getChildContext() method ' + + 'on the instance. You can either define getChildContext() on %s or remove ' + + 'childContextTypes from it.', + componentName, + componentName, + ); + } + } + return parentContext; + } + + const childContext = instance.getChildContext(); + for (const contextKey in childContext) { + invariant( + contextKey in childContextTypes, + '%s.getChildContext(): key "%s" is not defined in childContextTypes.', + getComponentName(type) || 'Unknown', + contextKey, + ); + } + if (__DEV__) { + const name = getComponentName(type) || 'Unknown'; + checkPropTypes(childContextTypes, childContext, 'child context', name); + } + + return {...parentContext, ...childContext}; + } +} + +function pushContextProvider(workInProgress: Fiber): boolean { + if (disableLegacyContext) { + return false; + } else { + const instance = workInProgress.stateNode; + // We push the context as early as possible to ensure stack integrity. + // If the instance does not exist yet, we will push null at first, + // and replace it on the stack later when invalidating the context. + const memoizedMergedChildContext = + (instance && instance.__reactInternalMemoizedMergedChildContext) || + emptyContextObject; + + // Remember the parent context so we can merge with it later. + // Inherit the parent's did-perform-work value to avoid inadvertently blocking updates. + previousContext = contextStackCursor.current; + push(contextStackCursor, memoizedMergedChildContext, workInProgress); + push( + didPerformWorkStackCursor, + didPerformWorkStackCursor.current, + workInProgress, + ); + + return true; + } +} + +function invalidateContextProvider( + workInProgress: Fiber, + type: any, + didChange: boolean, +): void { + if (disableLegacyContext) { + return; + } else { + const instance = workInProgress.stateNode; + invariant( + instance, + 'Expected to have an instance by this point. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + + if (didChange) { + // Merge parent and own context. + // Skip this if we're not updating due to sCU. + // This avoids unnecessarily recomputing memoized values. + const mergedContext = processChildContext( + workInProgress, + type, + previousContext, + ); + instance.__reactInternalMemoizedMergedChildContext = mergedContext; + + // Replace the old (or empty) context with the new one. + // It is important to unwind the context in the reverse order. + pop(didPerformWorkStackCursor, workInProgress); + pop(contextStackCursor, workInProgress); + // Now push the new context and mark that it has changed. + push(contextStackCursor, mergedContext, workInProgress); + push(didPerformWorkStackCursor, didChange, workInProgress); + } else { + pop(didPerformWorkStackCursor, workInProgress); + push(didPerformWorkStackCursor, didChange, workInProgress); + } + } +} + +function findCurrentUnmaskedContext(fiber: Fiber): Object { + if (disableLegacyContext) { + return emptyContextObject; + } else { + // Currently this is only used with renderSubtreeIntoContainer; not sure if it + // makes sense elsewhere + invariant( + isFiberMounted(fiber) && fiber.tag === ClassComponent, + 'Expected subtree parent to be a mounted class component. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + + let node = fiber; + do { + switch (node.tag) { + case HostRoot: + return node.stateNode.context; + case ClassComponent: { + const Component = node.type; + if (isContextProvider(Component)) { + return node.stateNode.__reactInternalMemoizedMergedChildContext; + } + break; + } + } + node = node.return; + } while (node !== null); + invariant( + false, + 'Found unexpected detached subtree parent. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + } +} + +export { + getUnmaskedContext, + cacheContext, + getMaskedContext, + hasContextChanged, + popContext, + popTopLevelContextObject, + pushTopLevelContextObject, + processChildContext, + isContextProvider, + pushContextProvider, + invalidateContextProvider, + findCurrentUnmaskedContext, +}; diff --git a/packages/react-reconciler/src/ReactFiberDeprecatedEvents.new.js b/packages/react-reconciler/src/ReactFiberDeprecatedEvents.new.js new file mode 100644 index 0000000000000..8f82f18cb9a51 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberDeprecatedEvents.new.js @@ -0,0 +1,234 @@ +/** + * 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 {Container, Instance} from './ReactFiberHostConfig'; +import type { + ReactEventResponder, + ReactEventResponderInstance, + ReactEventResponderListener, +} from 'shared/ReactTypes'; + +import { + DEPRECATED_mountResponderInstance, + DEPRECATED_unmountResponderInstance, +} from './ReactFiberHostConfig'; +import {NoWork} from './ReactFiberExpirationTime'; + +import {REACT_RESPONDER_TYPE} from 'shared/ReactSymbols'; + +import invariant from 'shared/invariant'; +import {HostComponent, HostRoot} from './ReactWorkTags'; + +const emptyObject = {}; +const isArray = Array.isArray; + +export function createResponderInstance( + responder: ReactEventResponder, + responderProps: Object, + responderState: Object, + fiber: Fiber, +): ReactEventResponderInstance { + return { + fiber, + props: responderProps, + responder, + rootEventTypes: null, + state: responderState, + }; +} + +function mountEventResponder( + responder: ReactEventResponder, + responderProps: Object, + fiber: Fiber, + respondersMap: Map< + ReactEventResponder, + ReactEventResponderInstance, + >, + rootContainerInstance: null | Container, +) { + let responderState = emptyObject; + const getInitialState = responder.getInitialState; + if (getInitialState !== null) { + responderState = getInitialState(responderProps); + } + const responderInstance = createResponderInstance( + responder, + responderProps, + responderState, + fiber, + ); + + if (!rootContainerInstance) { + let node = fiber; + while (node !== null) { + const tag = node.tag; + if (tag === HostComponent) { + rootContainerInstance = node.stateNode; + break; + } else if (tag === HostRoot) { + rootContainerInstance = node.stateNode.containerInfo; + break; + } + node = node.return; + } + } + + DEPRECATED_mountResponderInstance( + responder, + responderInstance, + responderProps, + responderState, + ((rootContainerInstance: any): Instance), + ); + respondersMap.set(responder, responderInstance); +} + +function updateEventListener( + listener: ReactEventResponderListener, + fiber: Fiber, + visistedResponders: Set>, + respondersMap: Map< + ReactEventResponder, + ReactEventResponderInstance, + >, + rootContainerInstance: null | Container, +): void { + let responder; + let props; + + if (listener) { + responder = listener.responder; + props = listener.props; + } + invariant( + responder && responder.$$typeof === REACT_RESPONDER_TYPE, + 'An invalid value was used as an event listener. Expect one or many event ' + + 'listeners created via React.unstable_useResponder().', + ); + const listenerProps = ((props: any): Object); + if (visistedResponders.has(responder)) { + // show warning + if (__DEV__) { + console.error( + 'Duplicate event responder "%s" found in event listeners. ' + + 'Event listeners passed to elements cannot use the same event responder more than once.', + responder.displayName, + ); + } + return; + } + visistedResponders.add(responder); + const responderInstance = respondersMap.get(responder); + + if (responderInstance === undefined) { + // Mount (happens in either complete or commit phase) + mountEventResponder( + responder, + listenerProps, + fiber, + respondersMap, + rootContainerInstance, + ); + } else { + // Update (happens during commit phase only) + responderInstance.props = listenerProps; + responderInstance.fiber = fiber; + } +} + +export function updateDeprecatedEventListeners( + listeners: any, + fiber: Fiber, + rootContainerInstance: null | Container, +): void { + const visistedResponders = new Set(); + let dependencies = fiber.dependencies; + if (listeners != null) { + if (dependencies === null) { + dependencies = fiber.dependencies = { + expirationTime: NoWork, + firstContext: null, + responders: new Map(), + }; + } + let respondersMap = dependencies.responders; + if (respondersMap === null) { + dependencies.responders = respondersMap = new Map(); + } + if (isArray(listeners)) { + for (let i = 0, length = listeners.length; i < length; i++) { + const listener = listeners[i]; + updateEventListener( + listener, + fiber, + visistedResponders, + respondersMap, + rootContainerInstance, + ); + } + } else { + updateEventListener( + listeners, + fiber, + visistedResponders, + respondersMap, + rootContainerInstance, + ); + } + } + if (dependencies !== null) { + const respondersMap = dependencies.responders; + if (respondersMap !== null) { + // Unmount + const mountedResponders = Array.from(respondersMap.keys()); + for (let i = 0, length = mountedResponders.length; i < length; i++) { + const mountedResponder = mountedResponders[i]; + if (!visistedResponders.has(mountedResponder)) { + const responderInstance = ((respondersMap.get( + mountedResponder, + ): any): ReactEventResponderInstance); + DEPRECATED_unmountResponderInstance(responderInstance); + respondersMap.delete(mountedResponder); + } + } + } + } +} + +export function createDeprecatedResponderListener( + responder: ReactEventResponder, + props: Object, +): ReactEventResponderListener { + const eventResponderListener = { + responder, + props, + }; + if (__DEV__) { + Object.freeze(eventResponderListener); + } + return eventResponderListener; +} + +export function unmountDeprecatedResponderListeners(fiber: Fiber) { + const dependencies = fiber.dependencies; + + if (dependencies !== null) { + const respondersMap = dependencies.responders; + if (respondersMap !== null) { + const responderInstances = Array.from(respondersMap.values()); + for (let i = 0, length = responderInstances.length; i < length; i++) { + const responderInstance = responderInstances[i]; + DEPRECATED_unmountResponderInstance(responderInstance); + } + dependencies.responders = null; + } + } +} diff --git a/packages/react-reconciler/src/ReactFiberDevToolsHook.new.js b/packages/react-reconciler/src/ReactFiberDevToolsHook.new.js new file mode 100644 index 0000000000000..e1623378b3577 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberDevToolsHook.new.js @@ -0,0 +1,128 @@ +/** + * 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 {enableProfilerTimer} from 'shared/ReactFeatureFlags'; +import {getCurrentTime} from './ReactFiberWorkLoop.new'; +import {inferPriorityFromExpirationTime} from './ReactFiberExpirationTime'; + +import type {Fiber} from './ReactInternalTypes'; +import type {FiberRoot} from './ReactInternalTypes'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type {ReactNodeList} from 'shared/ReactTypes'; + +import {DidCapture} from './ReactSideEffectTags'; + +declare var __REACT_DEVTOOLS_GLOBAL_HOOK__: Object | void; + +let rendererID = null; +let injectedHook = null; +let hasLoggedError = false; + +export const isDevToolsPresent = + typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined'; + +export function injectInternals(internals: Object): boolean { + if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') { + // No DevTools + return false; + } + const hook = __REACT_DEVTOOLS_GLOBAL_HOOK__; + if (hook.isDisabled) { + // This isn't a real property on the hook, but it can be set to opt out + // of DevTools integration and associated warnings and logs. + // https://github.com/facebook/react/issues/3877 + return true; + } + if (!hook.supportsFiber) { + if (__DEV__) { + console.error( + 'The installed version of React DevTools is too old and will not work ' + + 'with the current version of React. Please update React DevTools. ' + + 'https://fb.me/react-devtools', + ); + } + // DevTools exists, even though it doesn't support Fiber. + return true; + } + try { + rendererID = hook.inject(internals); + // We have successfully injected, so now it is safe to set up hooks. + injectedHook = hook; + } catch (err) { + // Catch all errors because it is unsafe to throw during initialization. + if (__DEV__) { + console.error('React instrumentation encountered an error: %s.', err); + } + } + // DevTools exists + return true; +} + +export function onScheduleRoot(root: FiberRoot, children: ReactNodeList) { + if (__DEV__) { + if ( + injectedHook && + typeof injectedHook.onScheduleFiberRoot === 'function' + ) { + try { + injectedHook.onScheduleFiberRoot(rendererID, root, children); + } catch (err) { + if (__DEV__ && !hasLoggedError) { + hasLoggedError = true; + console.error('React instrumentation encountered an error: %s', err); + } + } + } + } +} + +export function onCommitRoot(root: FiberRoot, expirationTime: ExpirationTime) { + if (injectedHook && typeof injectedHook.onCommitFiberRoot === 'function') { + try { + const didError = (root.current.effectTag & DidCapture) === DidCapture; + if (enableProfilerTimer) { + const currentTime = getCurrentTime(); + const priorityLevel = inferPriorityFromExpirationTime( + currentTime, + expirationTime, + ); + injectedHook.onCommitFiberRoot( + rendererID, + root, + priorityLevel, + didError, + ); + } else { + injectedHook.onCommitFiberRoot(rendererID, root, undefined, didError); + } + } catch (err) { + if (__DEV__) { + if (!hasLoggedError) { + hasLoggedError = true; + console.error('React instrumentation encountered an error: %s', err); + } + } + } + } +} + +export function onCommitUnmount(fiber: Fiber) { + if (injectedHook && typeof injectedHook.onCommitFiberUnmount === 'function') { + try { + injectedHook.onCommitFiberUnmount(rendererID, fiber); + } catch (err) { + if (__DEV__) { + if (!hasLoggedError) { + hasLoggedError = true; + console.error('React instrumentation encountered an error: %s', err); + } + } + } + } +} diff --git a/packages/react-reconciler/src/ReactFiberFundamental.new.js b/packages/react-reconciler/src/ReactFiberFundamental.new.js new file mode 100644 index 0000000000000..0107a92ae468b --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberFundamental.new.js @@ -0,0 +1,30 @@ +/** + * 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 { + ReactFundamentalImpl, + ReactFundamentalComponentInstance, +} from 'shared/ReactTypes'; + +export function createFundamentalStateInstance( + currentFiber: Fiber, + props: Object, + impl: ReactFundamentalImpl, + state: Object, +): ReactFundamentalComponentInstance { + return { + currentFiber, + impl, + instance: null, + prevProps: null, + props, + state, + }; +} diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js new file mode 100644 index 0000000000000..71e270ee2ef7c --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -0,0 +1,3003 @@ +/** + * 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 { + MutableSource, + MutableSourceGetSnapshotFn, + MutableSourceSubscribeFn, + ReactEventResponder, + ReactContext, + ReactEventResponderListener, + ReactScopeMethods, +} from 'shared/ReactTypes'; +import type {Fiber, Dispatcher} from './ReactInternalTypes'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type {HookEffectTag} from './ReactHookEffectTags'; +import type {SuspenseConfig} from './ReactFiberSuspenseConfig'; +import type {ReactPriorityLevel} from './ReactInternalTypes'; +import type {FiberRoot} from './ReactInternalTypes'; +import type { + OpaqueIDType, + ReactListenerEvent, + ReactListenerMap, + ReactListener, +} from './ReactFiberHostConfig'; + +import ReactSharedInternals from 'shared/ReactSharedInternals'; +import {enableUseEventAPI} from 'shared/ReactFeatureFlags'; + +import {markRootExpiredAtTime} from './ReactFiberRoot.new'; +import {NoWork, Sync} from './ReactFiberExpirationTime'; +import {NoMode, BlockingMode} from './ReactTypeOfMode'; +import {readContext} from './ReactFiberNewContext.new'; +import {createDeprecatedResponderListener} from './ReactFiberDeprecatedEvents.new'; +import { + Update as UpdateEffect, + Passive as PassiveEffect, +} from './ReactSideEffectTags'; +import { + NoEffect as NoHookEffect, + HasEffect as HookHasEffect, + Layout as HookLayout, + Passive as HookPassive, +} from './ReactHookEffectTags'; +import { + getWorkInProgressRoot, + scheduleUpdateOnFiber, + computeExpirationForFiber, + requestCurrentTimeForUpdate, + warnIfNotCurrentlyActingEffectsInDEV, + warnIfNotCurrentlyActingUpdatesInDev, + warnIfNotScopedWithMatchingAct, + markRenderEventTimeAndConfig, + markUnprocessedUpdateTime, +} from './ReactFiberWorkLoop.new'; +import { + registerEvent, + mountEventListener as mountHostEventListener, + unmountEventListener as unmountHostEventListener, + validateEventListenerTarget, +} from './ReactFiberHostConfig'; + +import invariant from 'shared/invariant'; +import getComponentName from 'shared/getComponentName'; +import is from 'shared/objectIs'; +import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork.new'; +import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig'; +import { + UserBlockingPriority, + NormalPriority, + runWithPriority, + getCurrentPriorityLevel, +} from './SchedulerWithReactIntegration.new'; +import {getIsHydrating} from './ReactFiberHydrationContext.new'; +import { + makeClientId, + makeClientIdInDEV, + makeOpaqueHydratingObject, +} from './ReactFiberHostConfig'; +import { + getLastPendingExpirationTime, + getWorkInProgressVersion, + markSourceAsDirty, + setPendingExpirationTime, + setWorkInProgressVersion, + warnAboutMultipleRenderersDEV, +} from './ReactMutableSource.new'; +import {getRootHostContainer} from './ReactFiberHostContext.new'; +import {getIsRendering} from './ReactCurrentFiber'; + +const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; + +type Update = {| + expirationTime: ExpirationTime, + suspenseConfig: null | SuspenseConfig, + action: A, + eagerReducer: ((S, A) => S) | null, + eagerState: S | null, + next: Update, + priority?: ReactPriorityLevel, +|}; + +type UpdateQueue = {| + pending: Update | null, + dispatch: (A => mixed) | null, + lastRenderedReducer: ((S, A) => S) | null, + lastRenderedState: S | null, +|}; + +export type HookType = + | 'useState' + | 'useReducer' + | 'useContext' + | 'useRef' + | 'useEffect' + | 'useLayoutEffect' + | 'useCallback' + | 'useMemo' + | 'useImperativeHandle' + | 'useDebugValue' + | 'useResponder' + | 'useDeferredValue' + | 'useTransition' + | 'useMutableSource' + | 'useEvent' + | 'useOpaqueIdentifier'; + +let didWarnAboutMismatchedHooksForComponent; +let didWarnAboutUseOpaqueIdentifier; +if (__DEV__) { + didWarnAboutUseOpaqueIdentifier = {}; + didWarnAboutMismatchedHooksForComponent = new Set(); +} + +export type Hook = {| + memoizedState: any, + baseState: any, + baseQueue: Update | null, + queue: UpdateQueue | null, + next: Hook | null, +|}; + +export type Effect = {| + tag: HookEffectTag, + create: () => (() => void) | void, + destroy: (() => void) | void, + deps: Array | null, + next: Effect, +|}; + +export type FunctionComponentUpdateQueue = {|lastEffect: Effect | null|}; + +type TimeoutConfig = {| + timeoutMs: number, +|}; + +type BasicStateAction = (S => S) | S; + +type Dispatch = A => void; + +// These are set right before calling the component. +let renderExpirationTime: ExpirationTime = NoWork; +// The work-in-progress fiber. I've named it differently to distinguish it from +// the work-in-progress hook. +let currentlyRenderingFiber: Fiber = (null: any); + +// Hooks are stored as a linked list on the fiber's memoizedState field. The +// current hook list is the list that belongs to the current fiber. The +// work-in-progress hook list is a new list that will be added to the +// work-in-progress fiber. +let currentHook: Hook | null = null; +let workInProgressHook: Hook | null = null; + +// Whether an update was scheduled at any point during the render phase. This +// does not get reset if we do another render pass; only when we're completely +// finished evaluating this component. This is an optimization so we know +// whether we need to clear render phase updates after a throw. +let didScheduleRenderPhaseUpdate: boolean = false; +// Where an update was scheduled only during the current render pass. This +// gets reset after each attempt. +// TODO: Maybe there's some way to consolidate this with +// `didScheduleRenderPhaseUpdate`. Or with `numberOfReRenders`. +let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false; + +const RE_RENDER_LIMIT = 25; + +// In DEV, this is the name of the currently executing primitive hook +let currentHookNameInDev: ?HookType = null; + +// In DEV, this list ensures that hooks are called in the same order between renders. +// The list stores the order of hooks used during the initial render (mount). +// Subsequent renders (updates) reference this list. +let hookTypesDev: Array | null = null; +let hookTypesUpdateIndexDev: number = -1; + +// In DEV, this tracks whether currently rendering component needs to ignore +// the dependencies for Hooks that need them (e.g. useEffect or useMemo). +// When true, such Hooks will always be "remounted". Only used during hot reload. +let ignorePreviousDependencies: boolean = false; + +function mountHookTypesDev() { + if (__DEV__) { + const hookName = ((currentHookNameInDev: any): HookType); + + if (hookTypesDev === null) { + hookTypesDev = [hookName]; + } else { + hookTypesDev.push(hookName); + } + } +} + +function updateHookTypesDev() { + if (__DEV__) { + const hookName = ((currentHookNameInDev: any): HookType); + + if (hookTypesDev !== null) { + hookTypesUpdateIndexDev++; + if (hookTypesDev[hookTypesUpdateIndexDev] !== hookName) { + warnOnHookMismatchInDev(hookName); + } + } + } +} + +function checkDepsAreArrayDev(deps: mixed) { + if (__DEV__) { + if (deps !== undefined && deps !== null && !Array.isArray(deps)) { + // Verify deps, but only on mount to avoid extra checks. + // It's unlikely their type would change as usually you define them inline. + console.error( + '%s received a final argument that is not an array (instead, received `%s`). When ' + + 'specified, the final argument must be an array.', + currentHookNameInDev, + typeof deps, + ); + } + } +} + +function warnOnHookMismatchInDev(currentHookName: HookType) { + if (__DEV__) { + const componentName = getComponentName(currentlyRenderingFiber.type); + if (!didWarnAboutMismatchedHooksForComponent.has(componentName)) { + didWarnAboutMismatchedHooksForComponent.add(componentName); + + if (hookTypesDev !== null) { + let table = ''; + + const secondColumnStart = 30; + + for (let i = 0; i <= ((hookTypesUpdateIndexDev: any): number); i++) { + const oldHookName = hookTypesDev[i]; + const newHookName = + i === ((hookTypesUpdateIndexDev: any): number) + ? currentHookName + : oldHookName; + + let row = `${i + 1}. ${oldHookName}`; + + // Extra space so second column lines up + // lol @ IE not supporting String#repeat + while (row.length < secondColumnStart) { + row += ' '; + } + + row += newHookName + '\n'; + + table += row; + } + + console.error( + 'React has detected a change in the order of Hooks called by %s. ' + + 'This will lead to bugs and errors if not fixed. ' + + 'For more information, read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' + + ' Previous render Next render\n' + + ' ------------------------------------------------------\n' + + '%s' + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n', + componentName, + table, + ); + } + } + } +} + +function throwInvalidHookError() { + invariant( + false, + 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' + + ' one of the following reasons:\n' + + '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + + '2. You might be breaking the Rules of Hooks\n' + + '3. You might have more than one copy of React in the same app\n' + + 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.', + ); +} + +function areHookInputsEqual( + nextDeps: Array, + prevDeps: Array | null, +) { + if (__DEV__) { + if (ignorePreviousDependencies) { + // Only true when this component is being hot reloaded. + return false; + } + } + + if (prevDeps === null) { + if (__DEV__) { + console.error( + '%s received a final argument during this render, but not during ' + + 'the previous render. Even though the final argument is optional, ' + + 'its type cannot change between renders.', + currentHookNameInDev, + ); + } + return false; + } + + if (__DEV__) { + // Don't bother comparing lengths in prod because these arrays should be + // passed inline. + if (nextDeps.length !== prevDeps.length) { + console.error( + 'The final argument passed to %s changed size between renders. The ' + + 'order and size of this array must remain constant.\n\n' + + 'Previous: %s\n' + + 'Incoming: %s', + currentHookNameInDev, + `[${prevDeps.join(', ')}]`, + `[${nextDeps.join(', ')}]`, + ); + } + } + for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) { + if (is(nextDeps[i], prevDeps[i])) { + continue; + } + return false; + } + return true; +} + +export function renderWithHooks( + current: Fiber | null, + workInProgress: Fiber, + Component: (p: Props, arg: SecondArg) => any, + props: Props, + secondArg: SecondArg, + nextRenderExpirationTime: ExpirationTime, +): any { + renderExpirationTime = nextRenderExpirationTime; + currentlyRenderingFiber = workInProgress; + + if (__DEV__) { + hookTypesDev = + current !== null + ? ((current._debugHookTypes: any): Array) + : null; + hookTypesUpdateIndexDev = -1; + // Used for hot reloading: + ignorePreviousDependencies = + current !== null && current.type !== workInProgress.type; + } + + workInProgress.memoizedState = null; + workInProgress.updateQueue = null; + workInProgress.expirationTime = NoWork; + + // The following should have already been reset + // currentHook = null; + // workInProgressHook = null; + + // didScheduleRenderPhaseUpdate = false; + + // TODO Warn if no hooks are used at all during mount, then some are used during update. + // Currently we will identify the update render as a mount because memoizedState === null. + // This is tricky because it's valid for certain types of components (e.g. React.lazy) + + // Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used. + // Non-stateful hooks (e.g. context) don't get added to memoizedState, + // so memoizedState would be null during updates and mounts. + if (__DEV__) { + if (current !== null && current.memoizedState !== null) { + ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV; + } else if (hookTypesDev !== null) { + // This dispatcher handles an edge case where a component is updating, + // but no stateful hooks have been used. + // We want to match the production code behavior (which will use HooksDispatcherOnMount), + // but with the extra DEV validation to ensure hooks ordering hasn't changed. + // This dispatcher does that. + ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV; + } else { + ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV; + } + } else { + ReactCurrentDispatcher.current = + current === null || current.memoizedState === null + ? HooksDispatcherOnMount + : HooksDispatcherOnUpdate; + } + + let children = Component(props, secondArg); + + // Check if there was a render phase update + if (didScheduleRenderPhaseUpdateDuringThisPass) { + // Keep rendering in a loop for as long as render phase updates continue to + // be scheduled. Use a counter to prevent infinite loops. + let numberOfReRenders: number = 0; + do { + didScheduleRenderPhaseUpdateDuringThisPass = false; + invariant( + numberOfReRenders < RE_RENDER_LIMIT, + 'Too many re-renders. React limits the number of renders to prevent ' + + 'an infinite loop.', + ); + + numberOfReRenders += 1; + if (__DEV__) { + // Even when hot reloading, allow dependencies to stabilize + // after first render to prevent infinite render phase updates. + ignorePreviousDependencies = false; + } + + // Start over from the beginning of the list + currentHook = null; + workInProgressHook = null; + + workInProgress.updateQueue = null; + + if (__DEV__) { + // Also validate hook order for cascading updates. + hookTypesUpdateIndexDev = -1; + } + + ReactCurrentDispatcher.current = __DEV__ + ? HooksDispatcherOnRerenderInDEV + : HooksDispatcherOnRerender; + + children = Component(props, secondArg); + } while (didScheduleRenderPhaseUpdateDuringThisPass); + } + + // We can assume the previous dispatcher is always this one, since we set it + // at the beginning of the render phase and there's no re-entrancy. + ReactCurrentDispatcher.current = ContextOnlyDispatcher; + + if (__DEV__) { + workInProgress._debugHookTypes = hookTypesDev; + } + + // This check uses currentHook so that it works the same in DEV and prod bundles. + // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles. + const didRenderTooFewHooks = + currentHook !== null && currentHook.next !== null; + + renderExpirationTime = NoWork; + currentlyRenderingFiber = (null: any); + + currentHook = null; + workInProgressHook = null; + + if (__DEV__) { + currentHookNameInDev = null; + hookTypesDev = null; + hookTypesUpdateIndexDev = -1; + } + + didScheduleRenderPhaseUpdate = false; + + invariant( + !didRenderTooFewHooks, + 'Rendered fewer hooks than expected. This may be caused by an accidental ' + + 'early return statement.', + ); + + return children; +} + +export function bailoutHooks( + current: Fiber, + workInProgress: Fiber, + expirationTime: ExpirationTime, +) { + workInProgress.updateQueue = current.updateQueue; + workInProgress.effectTag &= ~(PassiveEffect | UpdateEffect); + if (current.expirationTime <= expirationTime) { + current.expirationTime = NoWork; + } +} + +export function resetHooksAfterThrow(): void { + // We can assume the previous dispatcher is always this one, since we set it + // at the beginning of the render phase and there's no re-entrancy. + ReactCurrentDispatcher.current = ContextOnlyDispatcher; + + if (didScheduleRenderPhaseUpdate) { + // There were render phase updates. These are only valid for this render + // phase, which we are now aborting. Remove the updates from the queues so + // they do not persist to the next render. Do not remove updates from hooks + // that weren't processed. + // + // Only reset the updates from the queue if it has a clone. If it does + // not have a clone, that means it wasn't processed, and the updates were + // scheduled before we entered the render phase. + let hook: Hook | null = currentlyRenderingFiber.memoizedState; + while (hook !== null) { + const queue = hook.queue; + if (queue !== null) { + queue.pending = null; + } + hook = hook.next; + } + didScheduleRenderPhaseUpdate = false; + } + + renderExpirationTime = NoWork; + currentlyRenderingFiber = (null: any); + + currentHook = null; + workInProgressHook = null; + + if (__DEV__) { + hookTypesDev = null; + hookTypesUpdateIndexDev = -1; + + currentHookNameInDev = null; + + isUpdatingOpaqueValueInRenderPhase = false; + } + + didScheduleRenderPhaseUpdateDuringThisPass = false; +} + +function mountWorkInProgressHook(): Hook { + const hook: Hook = { + memoizedState: null, + + baseState: null, + baseQueue: null, + queue: null, + + next: null, + }; + + if (workInProgressHook === null) { + // This is the first hook in the list + currentlyRenderingFiber.memoizedState = workInProgressHook = hook; + } else { + // Append to the end of the list + workInProgressHook = workInProgressHook.next = hook; + } + return workInProgressHook; +} + +function updateWorkInProgressHook(): Hook { + // This function is used both for updates and for re-renders triggered by a + // render phase update. It assumes there is either a current hook we can + // clone, or a work-in-progress hook from a previous render pass that we can + // use as a base. When we reach the end of the base list, we must switch to + // the dispatcher used for mounts. + let nextCurrentHook: null | Hook; + if (currentHook === null) { + const current = currentlyRenderingFiber.alternate; + if (current !== null) { + nextCurrentHook = current.memoizedState; + } else { + nextCurrentHook = null; + } + } else { + nextCurrentHook = currentHook.next; + } + + let nextWorkInProgressHook: null | Hook; + if (workInProgressHook === null) { + nextWorkInProgressHook = currentlyRenderingFiber.memoizedState; + } else { + nextWorkInProgressHook = workInProgressHook.next; + } + + if (nextWorkInProgressHook !== null) { + // There's already a work-in-progress. Reuse it. + workInProgressHook = nextWorkInProgressHook; + nextWorkInProgressHook = workInProgressHook.next; + + currentHook = nextCurrentHook; + } else { + // Clone from the current hook. + + invariant( + nextCurrentHook !== null, + 'Rendered more hooks than during the previous render.', + ); + currentHook = nextCurrentHook; + + const newHook: Hook = { + memoizedState: currentHook.memoizedState, + + baseState: currentHook.baseState, + baseQueue: currentHook.baseQueue, + queue: currentHook.queue, + + next: null, + }; + + if (workInProgressHook === null) { + // This is the first hook in the list. + currentlyRenderingFiber.memoizedState = workInProgressHook = newHook; + } else { + // Append to the end of the list. + workInProgressHook = workInProgressHook.next = newHook; + } + } + return workInProgressHook; +} + +function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue { + return { + lastEffect: null, + }; +} + +function basicStateReducer(state: S, action: BasicStateAction): S { + // $FlowFixMe: Flow doesn't like mixed types + return typeof action === 'function' ? action(state) : action; +} + +function mountReducer( + reducer: (S, A) => S, + initialArg: I, + init?: I => S, +): [S, Dispatch] { + const hook = mountWorkInProgressHook(); + let initialState; + if (init !== undefined) { + initialState = init(initialArg); + } else { + initialState = ((initialArg: any): S); + } + hook.memoizedState = hook.baseState = initialState; + const queue = (hook.queue = { + pending: null, + dispatch: null, + lastRenderedReducer: reducer, + lastRenderedState: (initialState: any), + }); + const dispatch: Dispatch = (queue.dispatch = (dispatchAction.bind( + null, + currentlyRenderingFiber, + queue, + ): any)); + return [hook.memoizedState, dispatch]; +} + +function updateReducer( + reducer: (S, A) => S, + initialArg: I, + init?: I => S, +): [S, Dispatch] { + const hook = updateWorkInProgressHook(); + const queue = hook.queue; + invariant( + queue !== null, + 'Should have a queue. This is likely a bug in React. Please file an issue.', + ); + + queue.lastRenderedReducer = reducer; + + const current: Hook = (currentHook: any); + + // The last rebase update that is NOT part of the base state. + let baseQueue = current.baseQueue; + + // The last pending update that hasn't been processed yet. + const pendingQueue = queue.pending; + if (pendingQueue !== null) { + // We have new updates that haven't been processed yet. + // We'll add them to the base queue. + if (baseQueue !== null) { + // Merge the pending queue and the base queue. + const baseFirst = baseQueue.next; + const pendingFirst = pendingQueue.next; + baseQueue.next = pendingFirst; + pendingQueue.next = baseFirst; + } + if (__DEV__) { + if (current.baseQueue !== baseQueue) { + // Internal invariant that should never happen, but feasibly could in + // the future if we implement resuming, or some form of that. + console.error( + 'Internal error: Expected work-in-progress queue to be a clone. ' + + 'This is a bug in React.', + ); + } + } + current.baseQueue = baseQueue = pendingQueue; + queue.pending = null; + } + + if (baseQueue !== null) { + // We have a queue to process. + const first = baseQueue.next; + let newState = current.baseState; + + let newBaseState = null; + let newBaseQueueFirst = null; + let newBaseQueueLast = null; + let update = first; + do { + const updateExpirationTime = update.expirationTime; + if (updateExpirationTime < renderExpirationTime) { + // Priority is insufficient. Skip this update. If this is the first + // skipped update, the previous update/state is the new base + // update/state. + const clone: Update = { + expirationTime: update.expirationTime, + suspenseConfig: update.suspenseConfig, + action: update.action, + eagerReducer: update.eagerReducer, + eagerState: update.eagerState, + next: (null: any), + }; + if (newBaseQueueLast === null) { + newBaseQueueFirst = newBaseQueueLast = clone; + newBaseState = newState; + } else { + newBaseQueueLast = newBaseQueueLast.next = clone; + } + // Update the remaining priority in the queue. + if (updateExpirationTime > currentlyRenderingFiber.expirationTime) { + currentlyRenderingFiber.expirationTime = updateExpirationTime; + markUnprocessedUpdateTime(updateExpirationTime); + } + } else { + // This update does have sufficient priority. + + if (newBaseQueueLast !== null) { + const clone: Update = { + expirationTime: Sync, // This update is going to be committed so we never want uncommit it. + suspenseConfig: update.suspenseConfig, + action: update.action, + eagerReducer: update.eagerReducer, + eagerState: update.eagerState, + next: (null: any), + }; + newBaseQueueLast = newBaseQueueLast.next = clone; + } + + // Mark the event time of this update as relevant to this render pass. + // TODO: This should ideally use the true event time of this update rather than + // its priority which is a derived and not reverseable value. + // TODO: We should skip this update if it was already committed but currently + // we have no way of detecting the difference between a committed and suspended + // update here. + markRenderEventTimeAndConfig( + updateExpirationTime, + update.suspenseConfig, + ); + + // Process this update. + if (update.eagerReducer === reducer) { + // If this update was processed eagerly, and its reducer matches the + // current reducer, we can use the eagerly computed state. + newState = ((update.eagerState: any): S); + } else { + const action = update.action; + newState = reducer(newState, action); + } + } + update = update.next; + } while (update !== null && update !== first); + + if (newBaseQueueLast === null) { + newBaseState = newState; + } else { + newBaseQueueLast.next = (newBaseQueueFirst: any); + } + + // Mark that the fiber performed work, but only if the new state is + // different from the current state. + if (!is(newState, hook.memoizedState)) { + markWorkInProgressReceivedUpdate(); + } + + hook.memoizedState = newState; + hook.baseState = newBaseState; + hook.baseQueue = newBaseQueueLast; + + queue.lastRenderedState = newState; + } + + const dispatch: Dispatch = (queue.dispatch: any); + return [hook.memoizedState, dispatch]; +} + +function rerenderReducer( + reducer: (S, A) => S, + initialArg: I, + init?: I => S, +): [S, Dispatch] { + const hook = updateWorkInProgressHook(); + const queue = hook.queue; + invariant( + queue !== null, + 'Should have a queue. This is likely a bug in React. Please file an issue.', + ); + + queue.lastRenderedReducer = reducer; + + // This is a re-render. Apply the new render phase updates to the previous + // work-in-progress hook. + const dispatch: Dispatch = (queue.dispatch: any); + const lastRenderPhaseUpdate = queue.pending; + let newState = hook.memoizedState; + if (lastRenderPhaseUpdate !== null) { + // The queue doesn't persist past this render pass. + queue.pending = null; + + const firstRenderPhaseUpdate = lastRenderPhaseUpdate.next; + let update = firstRenderPhaseUpdate; + do { + // Process this render phase update. We don't have to check the + // priority because it will always be the same as the current + // render's. + const action = update.action; + newState = reducer(newState, action); + update = update.next; + } while (update !== firstRenderPhaseUpdate); + + // Mark that the fiber performed work, but only if the new state is + // different from the current state. + if (!is(newState, hook.memoizedState)) { + markWorkInProgressReceivedUpdate(); + } + + hook.memoizedState = newState; + // Don't persist the state accumulated from the render phase updates to + // the base state unless the queue is empty. + // TODO: Not sure if this is the desired semantics, but it's what we + // do for gDSFP. I can't remember why. + if (hook.baseQueue === null) { + hook.baseState = newState; + } + + queue.lastRenderedState = newState; + } + return [newState, dispatch]; +} + +type MutableSourceMemoizedState = {| + refs: { + getSnapshot: MutableSourceGetSnapshotFn, + setSnapshot: Snapshot => void, + }, + source: MutableSource, + subscribe: MutableSourceSubscribeFn, +|}; + +function readFromUnsubcribedMutableSource( + root: FiberRoot, + source: MutableSource, + getSnapshot: MutableSourceGetSnapshotFn, +): Snapshot { + if (__DEV__) { + warnAboutMultipleRenderersDEV(source); + } + + const getVersion = source._getVersion; + const version = getVersion(source._source); + + // Is it safe for this component to read from this source during the current render? + let isSafeToReadFromSource = false; + + // Check the version first. + // If this render has already been started with a specific version, + // we can use it alone to determine if we can safely read from the source. + const currentRenderVersion = getWorkInProgressVersion(source); + if (currentRenderVersion !== null) { + isSafeToReadFromSource = currentRenderVersion === version; + } else { + // If there's no version, then we should fallback to checking the update time. + const pendingExpirationTime = getLastPendingExpirationTime(root); + + if (pendingExpirationTime === NoWork) { + isSafeToReadFromSource = true; + } else { + // If the source has pending updates, we can use the current render's expiration + // time to determine if it's safe to read again from the source. + isSafeToReadFromSource = pendingExpirationTime >= renderExpirationTime; + } + + if (isSafeToReadFromSource) { + // If it's safe to read from this source during the current render, + // store the version in case other components read from it. + // A changed version number will let those components know to throw and restart the render. + setWorkInProgressVersion(source, version); + } + } + + if (isSafeToReadFromSource) { + return getSnapshot(source._source); + } else { + // This handles the special case of a mutable source being shared beween renderers. + // In that case, if the source is mutated between the first and second renderer, + // The second renderer don't know that it needs to reset the WIP version during unwind, + // (because the hook only marks sources as dirty if it's written to their WIP version). + // That would cause this tear check to throw again and eventually be visible to the user. + // We can avoid this infinite loop by explicitly marking the source as dirty. + // + // This can lead to tearing in the first renderer when it resumes, + // but there's nothing we can do about that (short of throwing here and refusing to continue the render). + markSourceAsDirty(source); + + invariant( + false, + 'Cannot read from mutable source during the current render without tearing. This is a bug in React. Please file an issue.', + ); + } +} + +function useMutableSource( + hook: Hook, + source: MutableSource, + getSnapshot: MutableSourceGetSnapshotFn, + subscribe: MutableSourceSubscribeFn, +): Snapshot { + const root = ((getWorkInProgressRoot(): any): FiberRoot); + invariant( + root !== null, + 'Expected a work-in-progress root. This is a bug in React. Please file an issue.', + ); + + const getVersion = source._getVersion; + const version = getVersion(source._source); + + const dispatcher = ReactCurrentDispatcher.current; + + // eslint-disable-next-line prefer-const + let [currentSnapshot, setSnapshot] = dispatcher.useState(() => + readFromUnsubcribedMutableSource(root, source, getSnapshot), + ); + let snapshot = currentSnapshot; + + // Grab a handle to the state hook as well. + // We use it to clear the pending update queue if we have a new source. + const stateHook = ((workInProgressHook: any): Hook); + + const memoizedState = ((hook.memoizedState: any): MutableSourceMemoizedState< + Source, + Snapshot, + >); + const refs = memoizedState.refs; + const prevGetSnapshot = refs.getSnapshot; + const prevSource = memoizedState.source; + const prevSubscribe = memoizedState.subscribe; + + const fiber = currentlyRenderingFiber; + + hook.memoizedState = ({ + refs, + source, + subscribe, + }: MutableSourceMemoizedState); + + // Sync the values needed by our subscription handler after each commit. + dispatcher.useEffect(() => { + refs.getSnapshot = getSnapshot; + + // Normally the dispatch function for a state hook never changes, + // but this hook recreates the queue in certain cases to avoid updates from stale sources. + // handleChange() below needs to reference the dispatch function without re-subscribing, + // so we use a ref to ensure that it always has the latest version. + refs.setSnapshot = setSnapshot; + + // Check for a possible change between when we last rendered now. + const maybeNewVersion = getVersion(source._source); + if (!is(version, maybeNewVersion)) { + const maybeNewSnapshot = getSnapshot(source._source); + if (!is(snapshot, maybeNewSnapshot)) { + setSnapshot(maybeNewSnapshot); + + const currentTime = requestCurrentTimeForUpdate(); + const suspenseConfig = requestCurrentSuspenseConfig(); + const expirationTime = computeExpirationForFiber( + currentTime, + fiber, + suspenseConfig, + ); + setPendingExpirationTime(root, expirationTime); + + // If the source mutated between render and now, + // there may be state updates already scheduled from the old getSnapshot. + // Those updates should not commit without this value. + // There is no mechanism currently to associate these updates though, + // so for now we fall back to synchronously flushing all pending updates. + // TODO: Improve this later. + markRootExpiredAtTime(root, getLastPendingExpirationTime(root)); + } + } + }, [getSnapshot, source, subscribe]); + + // If we got a new source or subscribe function, re-subscribe in a passive effect. + dispatcher.useEffect(() => { + const handleChange = () => { + const latestGetSnapshot = refs.getSnapshot; + const latestSetSnapshot = refs.setSnapshot; + + try { + latestSetSnapshot(latestGetSnapshot(source._source)); + + // Record a pending mutable source update with the same expiration time. + const currentTime = requestCurrentTimeForUpdate(); + const suspenseConfig = requestCurrentSuspenseConfig(); + const expirationTime = computeExpirationForFiber( + currentTime, + fiber, + suspenseConfig, + ); + + setPendingExpirationTime(root, expirationTime); + } catch (error) { + // A selector might throw after a source mutation. + // e.g. it might try to read from a part of the store that no longer exists. + // In this case we should still schedule an update with React. + // Worst case the selector will throw again and then an error boundary will handle it. + latestSetSnapshot( + (() => { + throw error; + }: any), + ); + } + }; + + const unsubscribe = subscribe(source._source, handleChange); + if (__DEV__) { + if (typeof unsubscribe !== 'function') { + console.error( + 'Mutable source subscribe function must return an unsubscribe function.', + ); + } + } + + return unsubscribe; + }, [source, subscribe]); + + // If any of the inputs to useMutableSource change, reading is potentially unsafe. + // + // If either the source or the subscription have changed we can't can't trust the update queue. + // Maybe the source changed in a way that the old subscription ignored but the new one depends on. + // + // If the getSnapshot function changed, we also shouldn't rely on the update queue. + // It's possible that the underlying source was mutated between the when the last "change" event fired, + // and when the current render (with the new getSnapshot function) is processed. + // + // In both cases, we need to throw away pending udpates (since they are no longer relevant) + // and treat reading from the source as we do in the mount case. + if ( + !is(prevGetSnapshot, getSnapshot) || + !is(prevSource, source) || + !is(prevSubscribe, subscribe) + ) { + // Create a new queue and setState method, + // So if there are interleaved updates, they get pushed to the older queue. + // When this becomes current, the previous queue and dispatch method will be discarded, + // including any interleaving updates that occur. + const newQueue = { + pending: null, + dispatch: null, + lastRenderedReducer: basicStateReducer, + lastRenderedState: snapshot, + }; + newQueue.dispatch = setSnapshot = (dispatchAction.bind( + null, + currentlyRenderingFiber, + newQueue, + ): any); + stateHook.queue = newQueue; + stateHook.baseQueue = null; + snapshot = readFromUnsubcribedMutableSource(root, source, getSnapshot); + stateHook.memoizedState = stateHook.baseState = snapshot; + } + + return snapshot; +} + +function mountMutableSource( + source: MutableSource, + getSnapshot: MutableSourceGetSnapshotFn, + subscribe: MutableSourceSubscribeFn, +): Snapshot { + const hook = mountWorkInProgressHook(); + hook.memoizedState = ({ + refs: { + getSnapshot, + setSnapshot: (null: any), + }, + source, + subscribe, + }: MutableSourceMemoizedState); + return useMutableSource(hook, source, getSnapshot, subscribe); +} + +function updateMutableSource( + source: MutableSource, + getSnapshot: MutableSourceGetSnapshotFn, + subscribe: MutableSourceSubscribeFn, +): Snapshot { + const hook = updateWorkInProgressHook(); + return useMutableSource(hook, source, getSnapshot, subscribe); +} + +function mountState( + initialState: (() => S) | S, +): [S, Dispatch>] { + const hook = mountWorkInProgressHook(); + if (typeof initialState === 'function') { + // $FlowFixMe: Flow doesn't like mixed types + initialState = initialState(); + } + hook.memoizedState = hook.baseState = initialState; + const queue = (hook.queue = { + pending: null, + dispatch: null, + lastRenderedReducer: basicStateReducer, + lastRenderedState: (initialState: any), + }); + const dispatch: Dispatch< + BasicStateAction, + > = (queue.dispatch = (dispatchAction.bind( + null, + currentlyRenderingFiber, + queue, + ): any)); + return [hook.memoizedState, dispatch]; +} + +function updateState( + initialState: (() => S) | S, +): [S, Dispatch>] { + return updateReducer(basicStateReducer, (initialState: any)); +} + +function rerenderState( + initialState: (() => S) | S, +): [S, Dispatch>] { + return rerenderReducer(basicStateReducer, (initialState: any)); +} + +function pushEffect(tag, create, destroy, deps) { + const effect: Effect = { + tag, + create, + destroy, + deps, + // Circular + next: (null: any), + }; + let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any); + if (componentUpdateQueue === null) { + componentUpdateQueue = createFunctionComponentUpdateQueue(); + currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any); + componentUpdateQueue.lastEffect = effect.next = effect; + } else { + const lastEffect = componentUpdateQueue.lastEffect; + if (lastEffect === null) { + componentUpdateQueue.lastEffect = effect.next = effect; + } else { + const firstEffect = lastEffect.next; + lastEffect.next = effect; + effect.next = firstEffect; + componentUpdateQueue.lastEffect = effect; + } + } + return effect; +} + +function mountRef(initialValue: T): {|current: T|} { + const hook = mountWorkInProgressHook(); + const ref = {current: initialValue}; + if (__DEV__) { + Object.seal(ref); + } + hook.memoizedState = ref; + return ref; +} + +function updateRef(initialValue: T): {|current: T|} { + const hook = updateWorkInProgressHook(); + return hook.memoizedState; +} + +function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void { + const hook = mountWorkInProgressHook(); + const nextDeps = deps === undefined ? null : deps; + currentlyRenderingFiber.effectTag |= fiberEffectTag; + hook.memoizedState = pushEffect( + HookHasEffect | hookEffectTag, + create, + undefined, + nextDeps, + ); +} + +function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void { + const hook = updateWorkInProgressHook(); + const nextDeps = deps === undefined ? null : deps; + let destroy = undefined; + + if (currentHook !== null) { + const prevEffect = currentHook.memoizedState; + destroy = prevEffect.destroy; + if (nextDeps !== null) { + const prevDeps = prevEffect.deps; + if (areHookInputsEqual(nextDeps, prevDeps)) { + pushEffect(hookEffectTag, create, destroy, nextDeps); + return; + } + } + } + + currentlyRenderingFiber.effectTag |= fiberEffectTag; + + hook.memoizedState = pushEffect( + HookHasEffect | hookEffectTag, + create, + destroy, + nextDeps, + ); +} + +function mountEffect( + create: () => (() => void) | void, + deps: Array | void | null, +): void { + if (__DEV__) { + // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests + if ('undefined' !== typeof jest) { + warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); + } + } + return mountEffectImpl( + UpdateEffect | PassiveEffect, + HookPassive, + create, + deps, + ); +} + +function updateEffect( + create: () => (() => void) | void, + deps: Array | void | null, +): void { + if (__DEV__) { + // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests + if ('undefined' !== typeof jest) { + warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); + } + } + return updateEffectImpl( + UpdateEffect | PassiveEffect, + HookPassive, + create, + deps, + ); +} + +function mountLayoutEffect( + create: () => (() => void) | void, + deps: Array | void | null, +): void { + return mountEffectImpl(UpdateEffect, HookLayout, create, deps); +} + +function updateLayoutEffect( + create: () => (() => void) | void, + deps: Array | void | null, +): void { + return updateEffectImpl(UpdateEffect, HookLayout, create, deps); +} + +function imperativeHandleEffect( + create: () => T, + ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void, +) { + if (typeof ref === 'function') { + const refCallback = ref; + const inst = create(); + refCallback(inst); + return () => { + refCallback(null); + }; + } else if (ref !== null && ref !== undefined) { + const refObject = ref; + if (__DEV__) { + if (!refObject.hasOwnProperty('current')) { + console.error( + 'Expected useImperativeHandle() first argument to either be a ' + + 'ref callback or React.createRef() object. Instead received: %s.', + 'an object with keys {' + Object.keys(refObject).join(', ') + '}', + ); + } + } + const inst = create(); + refObject.current = inst; + return () => { + refObject.current = null; + }; + } +} + +function mountImperativeHandle( + ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void, + create: () => T, + deps: Array | void | null, +): void { + if (__DEV__) { + if (typeof create !== 'function') { + console.error( + 'Expected useImperativeHandle() second argument to be a function ' + + 'that creates a handle. Instead received: %s.', + create !== null ? typeof create : 'null', + ); + } + } + + // TODO: If deps are provided, should we skip comparing the ref itself? + const effectDeps = + deps !== null && deps !== undefined ? deps.concat([ref]) : null; + + return mountEffectImpl( + UpdateEffect, + HookLayout, + imperativeHandleEffect.bind(null, create, ref), + effectDeps, + ); +} + +function updateImperativeHandle( + ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void, + create: () => T, + deps: Array | void | null, +): void { + if (__DEV__) { + if (typeof create !== 'function') { + console.error( + 'Expected useImperativeHandle() second argument to be a function ' + + 'that creates a handle. Instead received: %s.', + create !== null ? typeof create : 'null', + ); + } + } + + // TODO: If deps are provided, should we skip comparing the ref itself? + const effectDeps = + deps !== null && deps !== undefined ? deps.concat([ref]) : null; + + return updateEffectImpl( + UpdateEffect, + HookLayout, + imperativeHandleEffect.bind(null, create, ref), + effectDeps, + ); +} + +function mountDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { + // This hook is normally a no-op. + // The react-debug-hooks package injects its own implementation + // so that e.g. DevTools can display custom hook values. +} + +const updateDebugValue = mountDebugValue; + +function mountCallback(callback: T, deps: Array | void | null): T { + const hook = mountWorkInProgressHook(); + const nextDeps = deps === undefined ? null : deps; + hook.memoizedState = [callback, nextDeps]; + return callback; +} + +function updateCallback(callback: T, deps: Array | void | null): T { + const hook = updateWorkInProgressHook(); + const nextDeps = deps === undefined ? null : deps; + const prevState = hook.memoizedState; + if (prevState !== null) { + if (nextDeps !== null) { + const prevDeps: Array | null = prevState[1]; + if (areHookInputsEqual(nextDeps, prevDeps)) { + return prevState[0]; + } + } + } + hook.memoizedState = [callback, nextDeps]; + return callback; +} + +function mountMemo( + nextCreate: () => T, + deps: Array | void | null, +): T { + const hook = mountWorkInProgressHook(); + const nextDeps = deps === undefined ? null : deps; + const nextValue = nextCreate(); + hook.memoizedState = [nextValue, nextDeps]; + return nextValue; +} + +function updateMemo( + nextCreate: () => T, + deps: Array | void | null, +): T { + const hook = updateWorkInProgressHook(); + const nextDeps = deps === undefined ? null : deps; + const prevState = hook.memoizedState; + if (prevState !== null) { + // Assume these are defined. If they're not, areHookInputsEqual will warn. + if (nextDeps !== null) { + const prevDeps: Array | null = prevState[1]; + if (areHookInputsEqual(nextDeps, prevDeps)) { + return prevState[0]; + } + } + } + const nextValue = nextCreate(); + hook.memoizedState = [nextValue, nextDeps]; + return nextValue; +} + +function mountDeferredValue( + value: T, + config: TimeoutConfig | void | null, +): T { + const [prevValue, setValue] = mountState(value); + mountEffect(() => { + const previousConfig = ReactCurrentBatchConfig.suspense; + ReactCurrentBatchConfig.suspense = config === undefined ? null : config; + try { + setValue(value); + } finally { + ReactCurrentBatchConfig.suspense = previousConfig; + } + }, [value, config]); + return prevValue; +} + +function updateDeferredValue( + value: T, + config: TimeoutConfig | void | null, +): T { + const [prevValue, setValue] = updateState(value); + updateEffect(() => { + const previousConfig = ReactCurrentBatchConfig.suspense; + ReactCurrentBatchConfig.suspense = config === undefined ? null : config; + try { + setValue(value); + } finally { + ReactCurrentBatchConfig.suspense = previousConfig; + } + }, [value, config]); + return prevValue; +} + +function rerenderDeferredValue( + value: T, + config: TimeoutConfig | void | null, +): T { + const [prevValue, setValue] = rerenderState(value); + updateEffect(() => { + const previousConfig = ReactCurrentBatchConfig.suspense; + ReactCurrentBatchConfig.suspense = config === undefined ? null : config; + try { + setValue(value); + } finally { + ReactCurrentBatchConfig.suspense = previousConfig; + } + }, [value, config]); + return prevValue; +} + +function startTransition(setPending, config, callback) { + const priorityLevel = getCurrentPriorityLevel(); + runWithPriority( + priorityLevel < UserBlockingPriority ? UserBlockingPriority : priorityLevel, + () => { + setPending(true); + }, + ); + runWithPriority( + priorityLevel > NormalPriority ? NormalPriority : priorityLevel, + () => { + const previousConfig = ReactCurrentBatchConfig.suspense; + ReactCurrentBatchConfig.suspense = config === undefined ? null : config; + try { + setPending(false); + callback(); + } finally { + ReactCurrentBatchConfig.suspense = previousConfig; + } + }, + ); +} + +function mountTransition( + config: SuspenseConfig | void | null, +): [(() => void) => void, boolean] { + const [isPending, setPending] = mountState(false); + const start = mountCallback(startTransition.bind(null, setPending, config), [ + setPending, + config, + ]); + return [start, isPending]; +} + +function updateTransition( + config: SuspenseConfig | void | null, +): [(() => void) => void, boolean] { + const [isPending, setPending] = updateState(false); + const start = updateCallback(startTransition.bind(null, setPending, config), [ + setPending, + config, + ]); + return [start, isPending]; +} + +function rerenderTransition( + config: SuspenseConfig | void | null, +): [(() => void) => void, boolean] { + const [isPending, setPending] = rerenderState(false); + const start = updateCallback(startTransition.bind(null, setPending, config), [ + setPending, + config, + ]); + return [start, isPending]; +} + +let isUpdatingOpaqueValueInRenderPhase = false; +export function getIsUpdatingOpaqueValueInRenderPhaseInDEV(): boolean | void { + if (__DEV__) { + return isUpdatingOpaqueValueInRenderPhase; + } +} + +function warnOnOpaqueIdentifierAccessInDEV(fiber) { + if (__DEV__) { + // TODO: Should warn in effects and callbacks, too + const name = getComponentName(fiber.type) || 'Unknown'; + if (getIsRendering() && !didWarnAboutUseOpaqueIdentifier[name]) { + console.error( + 'The object passed back from useOpaqueIdentifier is meant to be ' + + 'passed through to attributes only. Do not read the ' + + 'value directly.', + ); + didWarnAboutUseOpaqueIdentifier[name] = true; + } + } +} + +function mountOpaqueIdentifier(): OpaqueIDType | void { + const makeId = __DEV__ + ? makeClientIdInDEV.bind( + null, + warnOnOpaqueIdentifierAccessInDEV.bind(null, currentlyRenderingFiber), + ) + : makeClientId; + + if (getIsHydrating()) { + let didUpgrade = false; + const fiber = currentlyRenderingFiber; + const readValue = () => { + if (!didUpgrade) { + // Only upgrade once. This works even inside the render phase because + // the update is added to a shared queue, which outlasts the + // in-progress render. + didUpgrade = true; + if (__DEV__) { + isUpdatingOpaqueValueInRenderPhase = true; + setId(makeId()); + isUpdatingOpaqueValueInRenderPhase = false; + warnOnOpaqueIdentifierAccessInDEV(fiber); + } else { + setId(makeId()); + } + } + invariant( + false, + 'The object passed back from useOpaqueIdentifier is meant to be ' + + 'passed through to attributes only. Do not read the value directly.', + ); + }; + const id = makeOpaqueHydratingObject(readValue); + + const setId = mountState(id)[1]; + + if ((currentlyRenderingFiber.mode & BlockingMode) === NoMode) { + currentlyRenderingFiber.effectTag |= UpdateEffect | PassiveEffect; + pushEffect( + HookHasEffect | HookPassive, + () => { + setId(makeId()); + }, + undefined, + null, + ); + } + return id; + } else { + const id = makeId(); + mountState(id); + return id; + } +} + +function updateOpaqueIdentifier(): OpaqueIDType | void { + const id = updateState(undefined)[0]; + return id; +} + +function rerenderOpaqueIdentifier(): OpaqueIDType | void { + const id = rerenderState(undefined)[0]; + return id; +} + +function dispatchAction( + fiber: Fiber, + queue: UpdateQueue, + action: A, +) { + if (__DEV__) { + if (typeof arguments[3] === 'function') { + console.error( + "State updates from the useState() and useReducer() Hooks don't support the " + + 'second callback argument. To execute a side effect after ' + + 'rendering, declare it in the component body with useEffect().', + ); + } + } + + const currentTime = requestCurrentTimeForUpdate(); + const suspenseConfig = requestCurrentSuspenseConfig(); + const expirationTime = computeExpirationForFiber( + currentTime, + fiber, + suspenseConfig, + ); + + const update: Update = { + expirationTime, + suspenseConfig, + action, + eagerReducer: null, + eagerState: null, + next: (null: any), + }; + + if (__DEV__) { + update.priority = getCurrentPriorityLevel(); + } + + // Append the update to the end of the list. + const pending = queue.pending; + if (pending === null) { + // This is the first update. Create a circular list. + update.next = update; + } else { + update.next = pending.next; + pending.next = update; + } + queue.pending = update; + + const alternate = fiber.alternate; + if ( + fiber === currentlyRenderingFiber || + (alternate !== null && alternate === currentlyRenderingFiber) + ) { + // This is a render phase update. Stash it in a lazily-created map of + // queue -> linked list of updates. After this render pass, we'll restart + // and apply the stashed updates on top of the work-in-progress hook. + didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true; + update.expirationTime = renderExpirationTime; + } else { + if ( + fiber.expirationTime === NoWork && + (alternate === null || alternate.expirationTime === NoWork) + ) { + // The queue is currently empty, which means we can eagerly compute the + // next state before entering the render phase. If the new state is the + // same as the current state, we may be able to bail out entirely. + const lastRenderedReducer = queue.lastRenderedReducer; + if (lastRenderedReducer !== null) { + let prevDispatcher; + if (__DEV__) { + prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; + } + try { + const currentState: S = (queue.lastRenderedState: any); + const eagerState = lastRenderedReducer(currentState, action); + // Stash the eagerly computed state, and the reducer used to compute + // it, on the update object. If the reducer hasn't changed by the + // time we enter the render phase, then the eager state can be used + // without calling the reducer again. + update.eagerReducer = lastRenderedReducer; + update.eagerState = eagerState; + if (is(eagerState, currentState)) { + // Fast path. We can bail out without scheduling React to re-render. + // It's still possible that we'll need to rebase this update later, + // if the component re-renders for a different reason and by that + // time the reducer has changed. + return; + } + } catch (error) { + // Suppress the error. It will throw again in the render phase. + } finally { + if (__DEV__) { + ReactCurrentDispatcher.current = prevDispatcher; + } + } + } + } + if (__DEV__) { + // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests + if ('undefined' !== typeof jest) { + warnIfNotScopedWithMatchingAct(fiber); + warnIfNotCurrentlyActingUpdatesInDev(fiber); + } + } + scheduleUpdateOnFiber(fiber, expirationTime); + } +} + +const noOpMount = () => {}; + +function validateNotInFunctionRender(): boolean { + if (currentlyRenderingFiber === null) { + return true; + } + if (__DEV__) { + console.warn( + 'Event listener methods from useEvent() cannot be called during render.' + + ' These methods should be called in an effect or event callback outside the render.', + ); + } + return false; +} + +function createReactListener( + event: ReactListenerEvent, + callback: Event => void, + target: EventTarget | ReactScopeMethods, + destroy: Node => void, +): ReactListener { + return { + callback, + destroy, + event, + target, + }; +} + +function mountEventListener(event: ReactListenerEvent): ReactListenerMap { + if (enableUseEventAPI) { + const hook = mountWorkInProgressHook(); + const listenerMap: Map< + EventTarget | ReactScopeMethods, + ReactListener, + > = new Map(); + const rootContainerInstance = getRootHostContainer(); + + // Register the event to the current root to ensure event + // replaying can pick up the event ahead of time. + registerEvent(event, rootContainerInstance); + + const clear = () => { + if (validateNotInFunctionRender()) { + const listeners = Array.from(listenerMap.values()); + for (let i = 0; i < listeners.length; i++) { + unmountHostEventListener(listeners[i]); + } + listenerMap.clear(); + } + }; + + const destroy = (target: Node) => { + // We don't need to call detachListenerFromInstance + // here as this method should only ever be called + // from renderers that need to remove the instance + // from the map representing an instance that still + // holds a reference to the listenerMap. This means + // things like "window" listeners on ReactDOM should + // never enter this call path as the the instance in + // those cases would be that of "window", which + // should be handled via an optimized route in the + // renderer, making less overhead here. If we change + // this heuristic we should update this path to make + // sure we call detachListenerFromInstance. + listenerMap.delete(target); + }; + + const reactListenerMap: ReactListenerMap = { + clear, + setListener( + target: EventTarget | ReactScopeMethods, + callback: ?(Event) => void, + ): void { + if ( + validateNotInFunctionRender() && + validateEventListenerTarget(target, callback) + ) { + let listener = listenerMap.get(target); + if (listener === undefined) { + if (callback == null) { + return; + } + listener = createReactListener(event, callback, target, destroy); + listenerMap.set(target, listener); + } else { + if (callback == null) { + listenerMap.delete(target); + unmountHostEventListener(listener); + return; + } + listener.callback = callback; + } + mountHostEventListener(listener); + } + }, + }; + // In order to clear up upon the hook unmounting, + // we ensure we set the effecrt tag so that we visit + // this effect in the commit phase, so we can handle + // clean-up accordingly. + currentlyRenderingFiber.effectTag |= UpdateEffect; + pushEffect(NoHookEffect, noOpMount, clear, null); + hook.memoizedState = [reactListenerMap, event, clear]; + return reactListenerMap; + } + // To make Flow not complain + return (undefined: any); +} + +function updateEventListener(event: ReactListenerEvent): ReactListenerMap { + if (enableUseEventAPI) { + const hook = updateWorkInProgressHook(); + const [reactListenerMap, memoizedEvent, clear] = hook.memoizedState; + if (__DEV__) { + if (memoizedEvent.type !== event.type) { + console.warn( + 'The event type argument passed to the useEvent() hook was different between renders.' + + ' The event type is static and should never change between renders.', + ); + } + if (memoizedEvent.capture !== event.capture) { + console.warn( + 'The "capture" option passed to the useEvent() hook was different between renders.' + + ' The "capture" option is static and should never change between renders.', + ); + } + if (memoizedEvent.priority !== event.priority) { + console.warn( + 'The "priority" option passed to the useEvent() hook was different between renders.' + + ' The "priority" option is static and should never change between renders.', + ); + } + if (memoizedEvent.passive !== event.passive) { + console.warn( + 'The "passive" option passed to the useEvent() hook was different between renders.' + + ' The "passive" option is static and should never change between renders.', + ); + } + } + // In order to clear up upon the hook unmounting, + // we ensure we set the effecrt tag so that we visit + // this effect in the commit phase, so we can handle + // clean-up accordingly. + currentlyRenderingFiber.effectTag |= UpdateEffect; + pushEffect(NoHookEffect, noOpMount, clear, null); + return reactListenerMap; + } + // To make Flow not complain + return (undefined: any); +} + +export const ContextOnlyDispatcher: Dispatcher = { + readContext, + + useCallback: throwInvalidHookError, + useContext: throwInvalidHookError, + useEffect: throwInvalidHookError, + useImperativeHandle: throwInvalidHookError, + useLayoutEffect: throwInvalidHookError, + useMemo: throwInvalidHookError, + useReducer: throwInvalidHookError, + useRef: throwInvalidHookError, + useState: throwInvalidHookError, + useDebugValue: throwInvalidHookError, + useResponder: throwInvalidHookError, + useDeferredValue: throwInvalidHookError, + useTransition: throwInvalidHookError, + useMutableSource: throwInvalidHookError, + useEvent: throwInvalidHookError, + useOpaqueIdentifier: throwInvalidHookError, +}; + +const HooksDispatcherOnMount: Dispatcher = { + readContext, + + useCallback: mountCallback, + useContext: readContext, + useEffect: mountEffect, + useImperativeHandle: mountImperativeHandle, + useLayoutEffect: mountLayoutEffect, + useMemo: mountMemo, + useReducer: mountReducer, + useRef: mountRef, + useState: mountState, + useDebugValue: mountDebugValue, + useResponder: createDeprecatedResponderListener, + useDeferredValue: mountDeferredValue, + useTransition: mountTransition, + useMutableSource: mountMutableSource, + useEvent: mountEventListener, + useOpaqueIdentifier: mountOpaqueIdentifier, +}; + +const HooksDispatcherOnUpdate: Dispatcher = { + readContext, + + useCallback: updateCallback, + useContext: readContext, + useEffect: updateEffect, + useImperativeHandle: updateImperativeHandle, + useLayoutEffect: updateLayoutEffect, + useMemo: updateMemo, + useReducer: updateReducer, + useRef: updateRef, + useState: updateState, + useDebugValue: updateDebugValue, + useResponder: createDeprecatedResponderListener, + useDeferredValue: updateDeferredValue, + useTransition: updateTransition, + useMutableSource: updateMutableSource, + useEvent: updateEventListener, + useOpaqueIdentifier: updateOpaqueIdentifier, +}; + +const HooksDispatcherOnRerender: Dispatcher = { + readContext, + + useCallback: updateCallback, + useContext: readContext, + useEffect: updateEffect, + useImperativeHandle: updateImperativeHandle, + useLayoutEffect: updateLayoutEffect, + useMemo: updateMemo, + useReducer: rerenderReducer, + useRef: updateRef, + useState: rerenderState, + useDebugValue: updateDebugValue, + useResponder: createDeprecatedResponderListener, + useDeferredValue: rerenderDeferredValue, + useTransition: rerenderTransition, + useMutableSource: updateMutableSource, + useEvent: updateEventListener, + useOpaqueIdentifier: rerenderOpaqueIdentifier, +}; + +let HooksDispatcherOnMountInDEV: Dispatcher | null = null; +let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null; +let HooksDispatcherOnUpdateInDEV: Dispatcher | null = null; +let HooksDispatcherOnRerenderInDEV: Dispatcher | null = null; +let InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher | null = null; +let InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher | null = null; +let InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher | null = null; + +if (__DEV__) { + const warnInvalidContextAccess = () => { + console.error( + 'Context can only be read while React is rendering. ' + + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + + 'In function components, you can read it directly in the function body, but not ' + + 'inside Hooks like useReducer() or useMemo().', + ); + }; + + const warnInvalidHookAccess = () => { + console.error( + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' + + 'You can only call Hooks at the top level of your React function. ' + + 'For more information, see ' + + 'https://fb.me/rules-of-hooks', + ); + }; + + HooksDispatcherOnMountInDEV = { + readContext( + context: ReactContext, + observedBits: void | number | boolean, + ): T { + return readContext(context, observedBits); + }, + useCallback(callback: T, deps: Array | void | null): T { + currentHookNameInDev = 'useCallback'; + mountHookTypesDev(); + checkDepsAreArrayDev(deps); + return mountCallback(callback, deps); + }, + useContext( + context: ReactContext, + observedBits: void | number | boolean, + ): T { + currentHookNameInDev = 'useContext'; + mountHookTypesDev(); + return readContext(context, observedBits); + }, + useEffect( + create: () => (() => void) | void, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useEffect'; + mountHookTypesDev(); + checkDepsAreArrayDev(deps); + return mountEffect(create, deps); + }, + useImperativeHandle( + ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void, + create: () => T, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useImperativeHandle'; + mountHookTypesDev(); + checkDepsAreArrayDev(deps); + return mountImperativeHandle(ref, create, deps); + }, + useLayoutEffect( + create: () => (() => void) | void, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useLayoutEffect'; + mountHookTypesDev(); + checkDepsAreArrayDev(deps); + return mountLayoutEffect(create, deps); + }, + useMemo(create: () => T, deps: Array | void | null): T { + currentHookNameInDev = 'useMemo'; + mountHookTypesDev(); + checkDepsAreArrayDev(deps); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; + try { + return mountMemo(create, deps); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useReducer( + reducer: (S, A) => S, + initialArg: I, + init?: I => S, + ): [S, Dispatch] { + currentHookNameInDev = 'useReducer'; + mountHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; + try { + return mountReducer(reducer, initialArg, init); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useRef(initialValue: T): {|current: T|} { + currentHookNameInDev = 'useRef'; + mountHookTypesDev(); + return mountRef(initialValue); + }, + useState( + initialState: (() => S) | S, + ): [S, Dispatch>] { + currentHookNameInDev = 'useState'; + mountHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; + try { + return mountState(initialState); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { + currentHookNameInDev = 'useDebugValue'; + mountHookTypesDev(); + return mountDebugValue(value, formatterFn); + }, + useResponder( + responder: ReactEventResponder, + props, + ): ReactEventResponderListener { + currentHookNameInDev = 'useResponder'; + mountHookTypesDev(); + return createDeprecatedResponderListener(responder, props); + }, + useDeferredValue(value: T, config: TimeoutConfig | void | null): T { + currentHookNameInDev = 'useDeferredValue'; + mountHookTypesDev(); + return mountDeferredValue(value, config); + }, + useTransition( + config: SuspenseConfig | void | null, + ): [(() => void) => void, boolean] { + currentHookNameInDev = 'useTransition'; + mountHookTypesDev(); + return mountTransition(config); + }, + useMutableSource( + source: MutableSource, + getSnapshot: MutableSourceGetSnapshotFn, + subscribe: MutableSourceSubscribeFn, + ): Snapshot { + currentHookNameInDev = 'useMutableSource'; + mountHookTypesDev(); + return mountMutableSource(source, getSnapshot, subscribe); + }, + useEvent(event: ReactListenerEvent): ReactListenerMap { + currentHookNameInDev = 'useEvent'; + mountHookTypesDev(); + return mountEventListener(event); + }, + useOpaqueIdentifier(): OpaqueIDType | void { + currentHookNameInDev = 'useOpaqueIdentifier'; + mountHookTypesDev(); + return mountOpaqueIdentifier(); + }, + }; + + HooksDispatcherOnMountWithHookTypesInDEV = { + readContext( + context: ReactContext, + observedBits: void | number | boolean, + ): T { + return readContext(context, observedBits); + }, + useCallback(callback: T, deps: Array | void | null): T { + currentHookNameInDev = 'useCallback'; + updateHookTypesDev(); + return mountCallback(callback, deps); + }, + useContext( + context: ReactContext, + observedBits: void | number | boolean, + ): T { + currentHookNameInDev = 'useContext'; + updateHookTypesDev(); + return readContext(context, observedBits); + }, + useEffect( + create: () => (() => void) | void, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useEffect'; + updateHookTypesDev(); + return mountEffect(create, deps); + }, + useImperativeHandle( + ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void, + create: () => T, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useImperativeHandle'; + updateHookTypesDev(); + return mountImperativeHandle(ref, create, deps); + }, + useLayoutEffect( + create: () => (() => void) | void, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useLayoutEffect'; + updateHookTypesDev(); + return mountLayoutEffect(create, deps); + }, + useMemo(create: () => T, deps: Array | void | null): T { + currentHookNameInDev = 'useMemo'; + updateHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; + try { + return mountMemo(create, deps); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useReducer( + reducer: (S, A) => S, + initialArg: I, + init?: I => S, + ): [S, Dispatch] { + currentHookNameInDev = 'useReducer'; + updateHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; + try { + return mountReducer(reducer, initialArg, init); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useRef(initialValue: T): {|current: T|} { + currentHookNameInDev = 'useRef'; + updateHookTypesDev(); + return mountRef(initialValue); + }, + useState( + initialState: (() => S) | S, + ): [S, Dispatch>] { + currentHookNameInDev = 'useState'; + updateHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; + try { + return mountState(initialState); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { + currentHookNameInDev = 'useDebugValue'; + updateHookTypesDev(); + return mountDebugValue(value, formatterFn); + }, + useResponder( + responder: ReactEventResponder, + props, + ): ReactEventResponderListener { + currentHookNameInDev = 'useResponder'; + updateHookTypesDev(); + return createDeprecatedResponderListener(responder, props); + }, + useDeferredValue(value: T, config: TimeoutConfig | void | null): T { + currentHookNameInDev = 'useDeferredValue'; + updateHookTypesDev(); + return mountDeferredValue(value, config); + }, + useTransition( + config: SuspenseConfig | void | null, + ): [(() => void) => void, boolean] { + currentHookNameInDev = 'useTransition'; + updateHookTypesDev(); + return mountTransition(config); + }, + useMutableSource( + source: MutableSource, + getSnapshot: MutableSourceGetSnapshotFn, + subscribe: MutableSourceSubscribeFn, + ): Snapshot { + currentHookNameInDev = 'useMutableSource'; + updateHookTypesDev(); + return mountMutableSource(source, getSnapshot, subscribe); + }, + useEvent(event: ReactListenerEvent): ReactListenerMap { + currentHookNameInDev = 'useEvent'; + updateHookTypesDev(); + return mountEventListener(event); + }, + useOpaqueIdentifier(): OpaqueIDType | void { + currentHookNameInDev = 'useOpaqueIdentifier'; + updateHookTypesDev(); + return mountOpaqueIdentifier(); + }, + }; + + HooksDispatcherOnUpdateInDEV = { + readContext( + context: ReactContext, + observedBits: void | number | boolean, + ): T { + return readContext(context, observedBits); + }, + useCallback(callback: T, deps: Array | void | null): T { + currentHookNameInDev = 'useCallback'; + updateHookTypesDev(); + return updateCallback(callback, deps); + }, + useContext( + context: ReactContext, + observedBits: void | number | boolean, + ): T { + currentHookNameInDev = 'useContext'; + updateHookTypesDev(); + return readContext(context, observedBits); + }, + useEffect( + create: () => (() => void) | void, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useEffect'; + updateHookTypesDev(); + return updateEffect(create, deps); + }, + useImperativeHandle( + ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void, + create: () => T, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useImperativeHandle'; + updateHookTypesDev(); + return updateImperativeHandle(ref, create, deps); + }, + useLayoutEffect( + create: () => (() => void) | void, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useLayoutEffect'; + updateHookTypesDev(); + return updateLayoutEffect(create, deps); + }, + useMemo(create: () => T, deps: Array | void | null): T { + currentHookNameInDev = 'useMemo'; + updateHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; + try { + return updateMemo(create, deps); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useReducer( + reducer: (S, A) => S, + initialArg: I, + init?: I => S, + ): [S, Dispatch] { + currentHookNameInDev = 'useReducer'; + updateHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; + try { + return updateReducer(reducer, initialArg, init); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useRef(initialValue: T): {|current: T|} { + currentHookNameInDev = 'useRef'; + updateHookTypesDev(); + return updateRef(initialValue); + }, + useState( + initialState: (() => S) | S, + ): [S, Dispatch>] { + currentHookNameInDev = 'useState'; + updateHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; + try { + return updateState(initialState); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { + currentHookNameInDev = 'useDebugValue'; + updateHookTypesDev(); + return updateDebugValue(value, formatterFn); + }, + useResponder( + responder: ReactEventResponder, + props, + ): ReactEventResponderListener { + currentHookNameInDev = 'useResponder'; + updateHookTypesDev(); + return createDeprecatedResponderListener(responder, props); + }, + useDeferredValue(value: T, config: TimeoutConfig | void | null): T { + currentHookNameInDev = 'useDeferredValue'; + updateHookTypesDev(); + return updateDeferredValue(value, config); + }, + useTransition( + config: SuspenseConfig | void | null, + ): [(() => void) => void, boolean] { + currentHookNameInDev = 'useTransition'; + updateHookTypesDev(); + return updateTransition(config); + }, + useMutableSource( + source: MutableSource, + getSnapshot: MutableSourceGetSnapshotFn, + subscribe: MutableSourceSubscribeFn, + ): Snapshot { + currentHookNameInDev = 'useMutableSource'; + updateHookTypesDev(); + return updateMutableSource(source, getSnapshot, subscribe); + }, + useEvent(event: ReactListenerEvent): ReactListenerMap { + currentHookNameInDev = 'useEvent'; + updateHookTypesDev(); + return updateEventListener(event); + }, + useOpaqueIdentifier(): OpaqueIDType | void { + currentHookNameInDev = 'useOpaqueIdentifier'; + updateHookTypesDev(); + return updateOpaqueIdentifier(); + }, + }; + + HooksDispatcherOnRerenderInDEV = { + readContext( + context: ReactContext, + observedBits: void | number | boolean, + ): T { + return readContext(context, observedBits); + }, + + useCallback(callback: T, deps: Array | void | null): T { + currentHookNameInDev = 'useCallback'; + updateHookTypesDev(); + return updateCallback(callback, deps); + }, + useContext( + context: ReactContext, + observedBits: void | number | boolean, + ): T { + currentHookNameInDev = 'useContext'; + updateHookTypesDev(); + return readContext(context, observedBits); + }, + useEffect( + create: () => (() => void) | void, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useEffect'; + updateHookTypesDev(); + return updateEffect(create, deps); + }, + useImperativeHandle( + ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void, + create: () => T, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useImperativeHandle'; + updateHookTypesDev(); + return updateImperativeHandle(ref, create, deps); + }, + useLayoutEffect( + create: () => (() => void) | void, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useLayoutEffect'; + updateHookTypesDev(); + return updateLayoutEffect(create, deps); + }, + useMemo(create: () => T, deps: Array | void | null): T { + currentHookNameInDev = 'useMemo'; + updateHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnRerenderInDEV; + try { + return updateMemo(create, deps); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useReducer( + reducer: (S, A) => S, + initialArg: I, + init?: I => S, + ): [S, Dispatch] { + currentHookNameInDev = 'useReducer'; + updateHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnRerenderInDEV; + try { + return rerenderReducer(reducer, initialArg, init); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useRef(initialValue: T): {|current: T|} { + currentHookNameInDev = 'useRef'; + updateHookTypesDev(); + return updateRef(initialValue); + }, + useState( + initialState: (() => S) | S, + ): [S, Dispatch>] { + currentHookNameInDev = 'useState'; + updateHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnRerenderInDEV; + try { + return rerenderState(initialState); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { + currentHookNameInDev = 'useDebugValue'; + updateHookTypesDev(); + return updateDebugValue(value, formatterFn); + }, + useResponder( + responder: ReactEventResponder, + props, + ): ReactEventResponderListener { + currentHookNameInDev = 'useResponder'; + updateHookTypesDev(); + return createDeprecatedResponderListener(responder, props); + }, + useDeferredValue(value: T, config: TimeoutConfig | void | null): T { + currentHookNameInDev = 'useDeferredValue'; + updateHookTypesDev(); + return rerenderDeferredValue(value, config); + }, + useTransition( + config: SuspenseConfig | void | null, + ): [(() => void) => void, boolean] { + currentHookNameInDev = 'useTransition'; + updateHookTypesDev(); + return rerenderTransition(config); + }, + useMutableSource( + source: MutableSource, + getSnapshot: MutableSourceGetSnapshotFn, + subscribe: MutableSourceSubscribeFn, + ): Snapshot { + currentHookNameInDev = 'useMutableSource'; + updateHookTypesDev(); + return updateMutableSource(source, getSnapshot, subscribe); + }, + useEvent(event: ReactListenerEvent): ReactListenerMap { + currentHookNameInDev = 'useEvent'; + updateHookTypesDev(); + return updateEventListener(event); + }, + useOpaqueIdentifier(): OpaqueIDType | void { + currentHookNameInDev = 'useOpaqueIdentifier'; + updateHookTypesDev(); + return rerenderOpaqueIdentifier(); + }, + }; + + InvalidNestedHooksDispatcherOnMountInDEV = { + readContext( + context: ReactContext, + observedBits: void | number | boolean, + ): T { + warnInvalidContextAccess(); + return readContext(context, observedBits); + }, + useCallback(callback: T, deps: Array | void | null): T { + currentHookNameInDev = 'useCallback'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountCallback(callback, deps); + }, + useContext( + context: ReactContext, + observedBits: void | number | boolean, + ): T { + currentHookNameInDev = 'useContext'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return readContext(context, observedBits); + }, + useEffect( + create: () => (() => void) | void, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useEffect'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountEffect(create, deps); + }, + useImperativeHandle( + ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void, + create: () => T, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useImperativeHandle'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountImperativeHandle(ref, create, deps); + }, + useLayoutEffect( + create: () => (() => void) | void, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useLayoutEffect'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountLayoutEffect(create, deps); + }, + useMemo(create: () => T, deps: Array | void | null): T { + currentHookNameInDev = 'useMemo'; + warnInvalidHookAccess(); + mountHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; + try { + return mountMemo(create, deps); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useReducer( + reducer: (S, A) => S, + initialArg: I, + init?: I => S, + ): [S, Dispatch] { + currentHookNameInDev = 'useReducer'; + warnInvalidHookAccess(); + mountHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; + try { + return mountReducer(reducer, initialArg, init); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useRef(initialValue: T): {|current: T|} { + currentHookNameInDev = 'useRef'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountRef(initialValue); + }, + useState( + initialState: (() => S) | S, + ): [S, Dispatch>] { + currentHookNameInDev = 'useState'; + warnInvalidHookAccess(); + mountHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; + try { + return mountState(initialState); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { + currentHookNameInDev = 'useDebugValue'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountDebugValue(value, formatterFn); + }, + useResponder( + responder: ReactEventResponder, + props, + ): ReactEventResponderListener { + currentHookNameInDev = 'useResponder'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return createDeprecatedResponderListener(responder, props); + }, + useDeferredValue(value: T, config: TimeoutConfig | void | null): T { + currentHookNameInDev = 'useDeferredValue'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountDeferredValue(value, config); + }, + useTransition( + config: SuspenseConfig | void | null, + ): [(() => void) => void, boolean] { + currentHookNameInDev = 'useTransition'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountTransition(config); + }, + useMutableSource( + source: MutableSource, + getSnapshot: MutableSourceGetSnapshotFn, + subscribe: MutableSourceSubscribeFn, + ): Snapshot { + currentHookNameInDev = 'useMutableSource'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountMutableSource(source, getSnapshot, subscribe); + }, + useEvent(event: ReactListenerEvent): ReactListenerMap { + currentHookNameInDev = 'useEvent'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountEventListener(event); + }, + useOpaqueIdentifier(): OpaqueIDType | void { + currentHookNameInDev = 'useOpaqueIdentifier'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountOpaqueIdentifier(); + }, + }; + + InvalidNestedHooksDispatcherOnUpdateInDEV = { + readContext( + context: ReactContext, + observedBits: void | number | boolean, + ): T { + warnInvalidContextAccess(); + return readContext(context, observedBits); + }, + useCallback(callback: T, deps: Array | void | null): T { + currentHookNameInDev = 'useCallback'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateCallback(callback, deps); + }, + useContext( + context: ReactContext, + observedBits: void | number | boolean, + ): T { + currentHookNameInDev = 'useContext'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return readContext(context, observedBits); + }, + useEffect( + create: () => (() => void) | void, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useEffect'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateEffect(create, deps); + }, + useImperativeHandle( + ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void, + create: () => T, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useImperativeHandle'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateImperativeHandle(ref, create, deps); + }, + useLayoutEffect( + create: () => (() => void) | void, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useLayoutEffect'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateLayoutEffect(create, deps); + }, + useMemo(create: () => T, deps: Array | void | null): T { + currentHookNameInDev = 'useMemo'; + warnInvalidHookAccess(); + updateHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; + try { + return updateMemo(create, deps); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useReducer( + reducer: (S, A) => S, + initialArg: I, + init?: I => S, + ): [S, Dispatch] { + currentHookNameInDev = 'useReducer'; + warnInvalidHookAccess(); + updateHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; + try { + return updateReducer(reducer, initialArg, init); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useRef(initialValue: T): {|current: T|} { + currentHookNameInDev = 'useRef'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateRef(initialValue); + }, + useState( + initialState: (() => S) | S, + ): [S, Dispatch>] { + currentHookNameInDev = 'useState'; + warnInvalidHookAccess(); + updateHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; + try { + return updateState(initialState); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { + currentHookNameInDev = 'useDebugValue'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateDebugValue(value, formatterFn); + }, + useResponder( + responder: ReactEventResponder, + props, + ): ReactEventResponderListener { + currentHookNameInDev = 'useResponder'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return createDeprecatedResponderListener(responder, props); + }, + useDeferredValue(value: T, config: TimeoutConfig | void | null): T { + currentHookNameInDev = 'useDeferredValue'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateDeferredValue(value, config); + }, + useTransition( + config: SuspenseConfig | void | null, + ): [(() => void) => void, boolean] { + currentHookNameInDev = 'useTransition'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateTransition(config); + }, + useMutableSource( + source: MutableSource, + getSnapshot: MutableSourceGetSnapshotFn, + subscribe: MutableSourceSubscribeFn, + ): Snapshot { + currentHookNameInDev = 'useMutableSource'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateMutableSource(source, getSnapshot, subscribe); + }, + useEvent(event: ReactListenerEvent): ReactListenerMap { + currentHookNameInDev = 'useEvent'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateEventListener(event); + }, + useOpaqueIdentifier(): OpaqueIDType | void { + currentHookNameInDev = 'useOpaqueIdentifier'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateOpaqueIdentifier(); + }, + }; + + InvalidNestedHooksDispatcherOnRerenderInDEV = { + readContext( + context: ReactContext, + observedBits: void | number | boolean, + ): T { + warnInvalidContextAccess(); + return readContext(context, observedBits); + }, + + useCallback(callback: T, deps: Array | void | null): T { + currentHookNameInDev = 'useCallback'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateCallback(callback, deps); + }, + useContext( + context: ReactContext, + observedBits: void | number | boolean, + ): T { + currentHookNameInDev = 'useContext'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return readContext(context, observedBits); + }, + useEffect( + create: () => (() => void) | void, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useEffect'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateEffect(create, deps); + }, + useImperativeHandle( + ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void, + create: () => T, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useImperativeHandle'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateImperativeHandle(ref, create, deps); + }, + useLayoutEffect( + create: () => (() => void) | void, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useLayoutEffect'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateLayoutEffect(create, deps); + }, + useMemo(create: () => T, deps: Array | void | null): T { + currentHookNameInDev = 'useMemo'; + warnInvalidHookAccess(); + updateHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; + try { + return updateMemo(create, deps); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useReducer( + reducer: (S, A) => S, + initialArg: I, + init?: I => S, + ): [S, Dispatch] { + currentHookNameInDev = 'useReducer'; + warnInvalidHookAccess(); + updateHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; + try { + return rerenderReducer(reducer, initialArg, init); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useRef(initialValue: T): {|current: T|} { + currentHookNameInDev = 'useRef'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateRef(initialValue); + }, + useState( + initialState: (() => S) | S, + ): [S, Dispatch>] { + currentHookNameInDev = 'useState'; + warnInvalidHookAccess(); + updateHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; + try { + return rerenderState(initialState); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { + currentHookNameInDev = 'useDebugValue'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateDebugValue(value, formatterFn); + }, + useResponder( + responder: ReactEventResponder, + props, + ): ReactEventResponderListener { + currentHookNameInDev = 'useResponder'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return createDeprecatedResponderListener(responder, props); + }, + useDeferredValue(value: T, config: TimeoutConfig | void | null): T { + currentHookNameInDev = 'useDeferredValue'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return rerenderDeferredValue(value, config); + }, + useTransition( + config: SuspenseConfig | void | null, + ): [(() => void) => void, boolean] { + currentHookNameInDev = 'useTransition'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return rerenderTransition(config); + }, + useMutableSource( + source: MutableSource, + getSnapshot: MutableSourceGetSnapshotFn, + subscribe: MutableSourceSubscribeFn, + ): Snapshot { + currentHookNameInDev = 'useMutableSource'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateMutableSource(source, getSnapshot, subscribe); + }, + useEvent(event: ReactListenerEvent): ReactListenerMap { + currentHookNameInDev = 'useEvent'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateEventListener(event); + }, + useOpaqueIdentifier(): OpaqueIDType | void { + currentHookNameInDev = 'useOpaqueIdentifier'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return rerenderOpaqueIdentifier(); + }, + }; +} diff --git a/packages/react-reconciler/src/ReactFiberHostContext.new.js b/packages/react-reconciler/src/ReactFiberHostContext.new.js new file mode 100644 index 0000000000000..298a96afecac6 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberHostContext.new.js @@ -0,0 +1,113 @@ +/** + * 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 {StackCursor} from './ReactFiberStack.new'; +import type {Container, HostContext} from './ReactFiberHostConfig'; + +import invariant from 'shared/invariant'; + +import {getChildHostContext, getRootHostContext} from './ReactFiberHostConfig'; +import {createCursor, push, pop} from './ReactFiberStack.new'; + +declare class NoContextT {} +const NO_CONTEXT: NoContextT = ({}: any); + +const contextStackCursor: StackCursor = createCursor( + NO_CONTEXT, +); +const contextFiberStackCursor: StackCursor = createCursor( + NO_CONTEXT, +); +const rootInstanceStackCursor: StackCursor< + Container | NoContextT, +> = createCursor(NO_CONTEXT); + +function requiredContext(c: Value | NoContextT): Value { + invariant( + c !== NO_CONTEXT, + 'Expected host context to exist. This error is likely caused by a bug ' + + 'in React. Please file an issue.', + ); + return (c: any); +} + +function getRootHostContainer(): Container { + const rootInstance = requiredContext(rootInstanceStackCursor.current); + return rootInstance; +} + +function pushHostContainer(fiber: Fiber, nextRootInstance: Container) { + // Push current root instance onto the stack; + // This allows us to reset root when portals are popped. + push(rootInstanceStackCursor, nextRootInstance, fiber); + // Track the context and the Fiber that provided it. + // This enables us to pop only Fibers that provide unique contexts. + push(contextFiberStackCursor, fiber, fiber); + + // Finally, we need to push the host context to the stack. + // However, we can't just call getRootHostContext() and push it because + // we'd have a different number of entries on the stack depending on + // whether getRootHostContext() throws somewhere in renderer code or not. + // So we push an empty value first. This lets us safely unwind on errors. + push(contextStackCursor, NO_CONTEXT, fiber); + const nextRootContext = getRootHostContext(nextRootInstance); + // Now that we know this function doesn't throw, replace it. + pop(contextStackCursor, fiber); + push(contextStackCursor, nextRootContext, fiber); +} + +function popHostContainer(fiber: Fiber) { + pop(contextStackCursor, fiber); + pop(contextFiberStackCursor, fiber); + pop(rootInstanceStackCursor, fiber); +} + +function getHostContext(): HostContext { + const context = requiredContext(contextStackCursor.current); + return context; +} + +function pushHostContext(fiber: Fiber): void { + const rootInstance: Container = requiredContext( + rootInstanceStackCursor.current, + ); + const context: HostContext = requiredContext(contextStackCursor.current); + const nextContext = getChildHostContext(context, fiber.type, rootInstance); + + // Don't push this Fiber's context unless it's unique. + if (context === nextContext) { + return; + } + + // Track the context and the Fiber that provided it. + // This enables us to pop only Fibers that provide unique contexts. + push(contextFiberStackCursor, fiber, fiber); + push(contextStackCursor, nextContext, fiber); +} + +function popHostContext(fiber: Fiber): void { + // Do not pop unless this Fiber provided the current context. + // pushHostContext() only pushes Fibers that provide unique contexts. + if (contextFiberStackCursor.current !== fiber) { + return; + } + + pop(contextStackCursor, fiber); + pop(contextFiberStackCursor, fiber); +} + +export { + getHostContext, + getRootHostContainer, + popHostContainer, + popHostContext, + pushHostContainer, + pushHostContext, +}; diff --git a/packages/react-reconciler/src/ReactFiberHotReloading.new.js b/packages/react-reconciler/src/ReactFiberHotReloading.new.js new file mode 100644 index 0000000000000..048476ff0f55a --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberHotReloading.new.js @@ -0,0 +1,482 @@ +/** + * 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 {ReactElement} from 'shared/ReactElementType'; +import type {Fiber} from './ReactInternalTypes'; +import type {FiberRoot} from './ReactInternalTypes'; +import type {Instance} from './ReactFiberHostConfig'; +import type {ReactNodeList} from 'shared/ReactTypes'; + +import { + flushSync, + scheduleUpdateOnFiber, + flushPassiveEffects, +} from './ReactFiberWorkLoop.new'; +import {updateContainer, syncUpdates} from './ReactFiberReconciler.new'; +import {emptyContextObject} from './ReactFiberContext.new'; +import {Sync} from './ReactFiberExpirationTime'; +import { + ClassComponent, + FunctionComponent, + ForwardRef, + HostComponent, + HostPortal, + HostRoot, + MemoComponent, + SimpleMemoComponent, +} from './ReactWorkTags'; +import { + REACT_FORWARD_REF_TYPE, + REACT_MEMO_TYPE, + REACT_LAZY_TYPE, +} from 'shared/ReactSymbols'; + +export type Family = {| + current: any, +|}; + +export type RefreshUpdate = {| + staleFamilies: Set, + updatedFamilies: Set, +|}; + +// Resolves type to a family. +type RefreshHandler = any => Family | void; + +// Used by React Refresh runtime through DevTools Global Hook. +export type SetRefreshHandler = (handler: RefreshHandler | null) => void; +export type ScheduleRefresh = (root: FiberRoot, update: RefreshUpdate) => void; +export type ScheduleRoot = (root: FiberRoot, element: ReactNodeList) => void; +export type FindHostInstancesForRefresh = ( + root: FiberRoot, + families: Array, +) => Set; + +let resolveFamily: RefreshHandler | null = null; +// $FlowFixMe Flow gets confused by a WeakSet feature check below. +let failedBoundaries: WeakSet | null = null; + +export const setRefreshHandler = (handler: RefreshHandler | null): void => { + if (__DEV__) { + resolveFamily = handler; + } +}; + +export function resolveFunctionForHotReloading(type: any): any { + if (__DEV__) { + if (resolveFamily === null) { + // Hot reloading is disabled. + return type; + } + const family = resolveFamily(type); + if (family === undefined) { + return type; + } + // Use the latest known implementation. + return family.current; + } else { + return type; + } +} + +export function resolveClassForHotReloading(type: any): any { + // No implementation differences. + return resolveFunctionForHotReloading(type); +} + +export function resolveForwardRefForHotReloading(type: any): any { + if (__DEV__) { + if (resolveFamily === null) { + // Hot reloading is disabled. + return type; + } + const family = resolveFamily(type); + if (family === undefined) { + // Check if we're dealing with a real forwardRef. Don't want to crash early. + if ( + type !== null && + type !== undefined && + typeof type.render === 'function' + ) { + // ForwardRef is special because its resolved .type is an object, + // but it's possible that we only have its inner render function in the map. + // If that inner render function is different, we'll build a new forwardRef type. + const currentRender = resolveFunctionForHotReloading(type.render); + if (type.render !== currentRender) { + const syntheticType = { + $$typeof: REACT_FORWARD_REF_TYPE, + render: currentRender, + }; + if (type.displayName !== undefined) { + (syntheticType: any).displayName = type.displayName; + } + return syntheticType; + } + } + return type; + } + // Use the latest known implementation. + return family.current; + } else { + return type; + } +} + +export function isCompatibleFamilyForHotReloading( + fiber: Fiber, + element: ReactElement, +): boolean { + if (__DEV__) { + if (resolveFamily === null) { + // Hot reloading is disabled. + return false; + } + + const prevType = fiber.elementType; + const nextType = element.type; + + // If we got here, we know types aren't === equal. + let needsCompareFamilies = false; + + const $$typeofNextType = + typeof nextType === 'object' && nextType !== null + ? nextType.$$typeof + : null; + + switch (fiber.tag) { + case ClassComponent: { + if (typeof nextType === 'function') { + needsCompareFamilies = true; + } + break; + } + case FunctionComponent: { + if (typeof nextType === 'function') { + needsCompareFamilies = true; + } else if ($$typeofNextType === REACT_LAZY_TYPE) { + // We don't know the inner type yet. + // We're going to assume that the lazy inner type is stable, + // and so it is sufficient to avoid reconciling it away. + // We're not going to unwrap or actually use the new lazy type. + needsCompareFamilies = true; + } + break; + } + case ForwardRef: { + if ($$typeofNextType === REACT_FORWARD_REF_TYPE) { + needsCompareFamilies = true; + } else if ($$typeofNextType === REACT_LAZY_TYPE) { + needsCompareFamilies = true; + } + break; + } + case MemoComponent: + case SimpleMemoComponent: { + if ($$typeofNextType === REACT_MEMO_TYPE) { + // TODO: if it was but can no longer be simple, + // we shouldn't set this. + needsCompareFamilies = true; + } else if ($$typeofNextType === REACT_LAZY_TYPE) { + needsCompareFamilies = true; + } + break; + } + default: + return false; + } + + // Check if both types have a family and it's the same one. + if (needsCompareFamilies) { + // Note: memo() and forwardRef() we'll compare outer rather than inner type. + // This means both of them need to be registered to preserve state. + // If we unwrapped and compared the inner types for wrappers instead, + // then we would risk falsely saying two separate memo(Foo) + // calls are equivalent because they wrap the same Foo function. + const prevFamily = resolveFamily(prevType); + if (prevFamily !== undefined && prevFamily === resolveFamily(nextType)) { + return true; + } + } + return false; + } else { + return false; + } +} + +export function markFailedErrorBoundaryForHotReloading(fiber: Fiber) { + if (__DEV__) { + if (resolveFamily === null) { + // Hot reloading is disabled. + return; + } + if (typeof WeakSet !== 'function') { + return; + } + if (failedBoundaries === null) { + failedBoundaries = new WeakSet(); + } + failedBoundaries.add(fiber); + } +} + +export const scheduleRefresh: ScheduleRefresh = ( + root: FiberRoot, + update: RefreshUpdate, +): void => { + if (__DEV__) { + if (resolveFamily === null) { + // Hot reloading is disabled. + return; + } + const {staleFamilies, updatedFamilies} = update; + flushPassiveEffects(); + flushSync(() => { + scheduleFibersWithFamiliesRecursively( + root.current, + updatedFamilies, + staleFamilies, + ); + }); + } +}; + +export const scheduleRoot: ScheduleRoot = ( + root: FiberRoot, + element: ReactNodeList, +): void => { + if (__DEV__) { + if (root.context !== emptyContextObject) { + // Super edge case: root has a legacy _renderSubtree context + // but we don't know the parentComponent so we can't pass it. + // Just ignore. We'll delete this with _renderSubtree code path later. + return; + } + flushPassiveEffects(); + syncUpdates(() => { + updateContainer(element, root, null, null); + }); + } +}; + +function scheduleFibersWithFamiliesRecursively( + fiber: Fiber, + updatedFamilies: Set, + staleFamilies: Set, +) { + if (__DEV__) { + const {alternate, child, sibling, tag, type} = fiber; + + let candidateType = null; + switch (tag) { + case FunctionComponent: + case SimpleMemoComponent: + case ClassComponent: + candidateType = type; + break; + case ForwardRef: + candidateType = type.render; + break; + default: + break; + } + + if (resolveFamily === null) { + throw new Error('Expected resolveFamily to be set during hot reload.'); + } + + let needsRender = false; + let needsRemount = false; + if (candidateType !== null) { + const family = resolveFamily(candidateType); + if (family !== undefined) { + if (staleFamilies.has(family)) { + needsRemount = true; + } else if (updatedFamilies.has(family)) { + if (tag === ClassComponent) { + needsRemount = true; + } else { + needsRender = true; + } + } + } + } + if (failedBoundaries !== null) { + if ( + failedBoundaries.has(fiber) || + (alternate !== null && failedBoundaries.has(alternate)) + ) { + needsRemount = true; + } + } + + if (needsRemount) { + fiber._debugNeedsRemount = true; + } + if (needsRemount || needsRender) { + scheduleUpdateOnFiber(fiber, Sync); + } + if (child !== null && !needsRemount) { + scheduleFibersWithFamiliesRecursively( + child, + updatedFamilies, + staleFamilies, + ); + } + if (sibling !== null) { + scheduleFibersWithFamiliesRecursively( + sibling, + updatedFamilies, + staleFamilies, + ); + } + } +} + +export const findHostInstancesForRefresh: FindHostInstancesForRefresh = ( + root: FiberRoot, + families: Array, +): Set => { + if (__DEV__) { + const hostInstances = new Set(); + const types = new Set(families.map(family => family.current)); + findHostInstancesForMatchingFibersRecursively( + root.current, + types, + hostInstances, + ); + return hostInstances; + } else { + throw new Error( + 'Did not expect findHostInstancesForRefresh to be called in production.', + ); + } +}; + +function findHostInstancesForMatchingFibersRecursively( + fiber: Fiber, + types: Set, + hostInstances: Set, +) { + if (__DEV__) { + const {child, sibling, tag, type} = fiber; + + let candidateType = null; + switch (tag) { + case FunctionComponent: + case SimpleMemoComponent: + case ClassComponent: + candidateType = type; + break; + case ForwardRef: + candidateType = type.render; + break; + default: + break; + } + + let didMatch = false; + if (candidateType !== null) { + if (types.has(candidateType)) { + didMatch = true; + } + } + + if (didMatch) { + // We have a match. This only drills down to the closest host components. + // There's no need to search deeper because for the purpose of giving + // visual feedback, "flashing" outermost parent rectangles is sufficient. + findHostInstancesForFiberShallowly(fiber, hostInstances); + } else { + // If there's no match, maybe there will be one further down in the child tree. + if (child !== null) { + findHostInstancesForMatchingFibersRecursively( + child, + types, + hostInstances, + ); + } + } + + if (sibling !== null) { + findHostInstancesForMatchingFibersRecursively( + sibling, + types, + hostInstances, + ); + } + } +} + +function findHostInstancesForFiberShallowly( + fiber: Fiber, + hostInstances: Set, +): void { + if (__DEV__) { + const foundHostInstances = findChildHostInstancesForFiberShallowly( + fiber, + hostInstances, + ); + if (foundHostInstances) { + return; + } + // If we didn't find any host children, fallback to closest host parent. + let node = fiber; + while (true) { + switch (node.tag) { + case HostComponent: + hostInstances.add(node.stateNode); + return; + case HostPortal: + hostInstances.add(node.stateNode.containerInfo); + return; + case HostRoot: + hostInstances.add(node.stateNode.containerInfo); + return; + } + if (node.return === null) { + throw new Error('Expected to reach root first.'); + } + node = node.return; + } + } +} + +function findChildHostInstancesForFiberShallowly( + fiber: Fiber, + hostInstances: Set, +): boolean { + if (__DEV__) { + let node: Fiber = fiber; + let foundHostInstances = false; + while (true) { + if (node.tag === HostComponent) { + // We got a match. + foundHostInstances = true; + hostInstances.add(node.stateNode); + // There may still be more, so keep searching. + } else if (node.child !== null) { + node.child.return = node; + node = node.child; + continue; + } + if (node === fiber) { + return foundHostInstances; + } + while (node.sibling === null) { + if (node.return === null || node.return === fiber) { + return foundHostInstances; + } + node = node.return; + } + node.sibling.return = node.return; + node = node.sibling; + } + } + return false; +} diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js new file mode 100644 index 0000000000000..419ba05d94b23 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js @@ -0,0 +1,503 @@ +/** + * 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 { + Instance, + TextInstance, + HydratableInstance, + SuspenseInstance, + Container, + HostContext, +} from './ReactFiberHostConfig'; +import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; + +import { + HostComponent, + HostText, + HostRoot, + SuspenseComponent, +} from './ReactWorkTags'; +import {Deletion, Placement, Hydrating} from './ReactSideEffectTags'; +import invariant from 'shared/invariant'; + +import { + createFiberFromHostInstanceForDeletion, + createFiberFromDehydratedFragment, +} from './ReactFiber.new'; +import { + shouldSetTextContent, + supportsHydration, + canHydrateInstance, + canHydrateTextInstance, + canHydrateSuspenseInstance, + getNextHydratableSibling, + getFirstHydratableChild, + hydrateInstance, + hydrateTextInstance, + hydrateSuspenseInstance, + getNextHydratableInstanceAfterSuspenseInstance, + didNotMatchHydratedContainerTextInstance, + didNotMatchHydratedTextInstance, + didNotHydrateContainerInstance, + didNotHydrateInstance, + didNotFindHydratableContainerInstance, + didNotFindHydratableContainerTextInstance, + didNotFindHydratableContainerSuspenseInstance, + didNotFindHydratableInstance, + didNotFindHydratableTextInstance, + didNotFindHydratableSuspenseInstance, +} from './ReactFiberHostConfig'; +import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags'; +import {Never, NoWork} from './ReactFiberExpirationTime'; + +// The deepest Fiber on the stack involved in a hydration context. +// This may have been an insertion or a hydration. +let hydrationParentFiber: null | Fiber = null; +let nextHydratableInstance: null | HydratableInstance = null; +let isHydrating: boolean = false; + +function warnIfHydrating() { + if (__DEV__) { + if (isHydrating) { + console.error( + 'We should not be hydrating here. This is a bug in React. Please file a bug.', + ); + } + } +} + +function enterHydrationState(fiber: Fiber): boolean { + if (!supportsHydration) { + return false; + } + + const parentInstance = fiber.stateNode.containerInfo; + nextHydratableInstance = getFirstHydratableChild(parentInstance); + hydrationParentFiber = fiber; + isHydrating = true; + return true; +} + +function reenterHydrationStateFromDehydratedSuspenseInstance( + fiber: Fiber, + suspenseInstance: SuspenseInstance, +): boolean { + if (!supportsHydration) { + return false; + } + nextHydratableInstance = getNextHydratableSibling(suspenseInstance); + popToNextHostParent(fiber); + isHydrating = true; + return true; +} + +function deleteHydratableInstance( + returnFiber: Fiber, + instance: HydratableInstance, +) { + if (__DEV__) { + switch (returnFiber.tag) { + case HostRoot: + didNotHydrateContainerInstance( + returnFiber.stateNode.containerInfo, + instance, + ); + break; + case HostComponent: + didNotHydrateInstance( + returnFiber.type, + returnFiber.memoizedProps, + returnFiber.stateNode, + instance, + ); + break; + } + } + + const childToDelete = createFiberFromHostInstanceForDeletion(); + childToDelete.stateNode = instance; + childToDelete.return = returnFiber; + childToDelete.effectTag = Deletion; + + // This might seem like it belongs on progressedFirstDeletion. However, + // these children are not part of the reconciliation list of children. + // Even if we abort and rereconcile the children, that will try to hydrate + // again and the nodes are still in the host tree so these will be + // recreated. + if (returnFiber.lastEffect !== null) { + returnFiber.lastEffect.nextEffect = childToDelete; + returnFiber.lastEffect = childToDelete; + } else { + returnFiber.firstEffect = returnFiber.lastEffect = childToDelete; + } +} + +function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) { + fiber.effectTag = (fiber.effectTag & ~Hydrating) | Placement; + if (__DEV__) { + switch (returnFiber.tag) { + case HostRoot: { + const parentContainer = returnFiber.stateNode.containerInfo; + switch (fiber.tag) { + case HostComponent: + const type = fiber.type; + const props = fiber.pendingProps; + didNotFindHydratableContainerInstance(parentContainer, type, props); + break; + case HostText: + const text = fiber.pendingProps; + didNotFindHydratableContainerTextInstance(parentContainer, text); + break; + case SuspenseComponent: + didNotFindHydratableContainerSuspenseInstance(parentContainer); + break; + } + break; + } + case HostComponent: { + const parentType = returnFiber.type; + const parentProps = returnFiber.memoizedProps; + const parentInstance = returnFiber.stateNode; + switch (fiber.tag) { + case HostComponent: + const type = fiber.type; + const props = fiber.pendingProps; + didNotFindHydratableInstance( + parentType, + parentProps, + parentInstance, + type, + props, + ); + break; + case HostText: + const text = fiber.pendingProps; + didNotFindHydratableTextInstance( + parentType, + parentProps, + parentInstance, + text, + ); + break; + case SuspenseComponent: + didNotFindHydratableSuspenseInstance( + parentType, + parentProps, + parentInstance, + ); + break; + } + break; + } + default: + return; + } + } +} + +function tryHydrate(fiber, nextInstance) { + switch (fiber.tag) { + case HostComponent: { + const type = fiber.type; + const props = fiber.pendingProps; + const instance = canHydrateInstance(nextInstance, type, props); + if (instance !== null) { + fiber.stateNode = (instance: Instance); + return true; + } + return false; + } + case HostText: { + const text = fiber.pendingProps; + const textInstance = canHydrateTextInstance(nextInstance, text); + if (textInstance !== null) { + fiber.stateNode = (textInstance: TextInstance); + return true; + } + return false; + } + case SuspenseComponent: { + if (enableSuspenseServerRenderer) { + const suspenseInstance: null | SuspenseInstance = canHydrateSuspenseInstance( + nextInstance, + ); + if (suspenseInstance !== null) { + const suspenseState: SuspenseState = { + dehydrated: suspenseInstance, + baseTime: NoWork, + retryTime: Never, + }; + fiber.memoizedState = suspenseState; + // Store the dehydrated fragment as a child fiber. + // This simplifies the code for getHostSibling and deleting nodes, + // since it doesn't have to consider all Suspense boundaries and + // check if they're dehydrated ones or not. + const dehydratedFragment = createFiberFromDehydratedFragment( + suspenseInstance, + ); + dehydratedFragment.return = fiber; + fiber.child = dehydratedFragment; + return true; + } + } + return false; + } + default: + return false; + } +} + +function tryToClaimNextHydratableInstance(fiber: Fiber): void { + if (!isHydrating) { + return; + } + let nextInstance = nextHydratableInstance; + if (!nextInstance) { + // Nothing to hydrate. Make it an insertion. + insertNonHydratedInstance((hydrationParentFiber: any), fiber); + isHydrating = false; + hydrationParentFiber = fiber; + return; + } + const firstAttemptedInstance = nextInstance; + if (!tryHydrate(fiber, nextInstance)) { + // If we can't hydrate this instance let's try the next one. + // We use this as a heuristic. It's based on intuition and not data so it + // might be flawed or unnecessary. + nextInstance = getNextHydratableSibling(firstAttemptedInstance); + if (!nextInstance || !tryHydrate(fiber, nextInstance)) { + // Nothing to hydrate. Make it an insertion. + insertNonHydratedInstance((hydrationParentFiber: any), fiber); + isHydrating = false; + hydrationParentFiber = fiber; + return; + } + // We matched the next one, we'll now assume that the first one was + // superfluous and we'll delete it. Since we can't eagerly delete it + // we'll have to schedule a deletion. To do that, this node needs a dummy + // fiber associated with it. + deleteHydratableInstance( + (hydrationParentFiber: any), + firstAttemptedInstance, + ); + } + hydrationParentFiber = fiber; + nextHydratableInstance = getFirstHydratableChild((nextInstance: any)); +} + +function prepareToHydrateHostInstance( + fiber: Fiber, + rootContainerInstance: Container, + hostContext: HostContext, +): boolean { + if (!supportsHydration) { + invariant( + false, + 'Expected prepareToHydrateHostInstance() to never be called. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + } + + const instance: Instance = fiber.stateNode; + const updatePayload = hydrateInstance( + instance, + fiber.type, + fiber.memoizedProps, + rootContainerInstance, + hostContext, + fiber, + ); + // TODO: Type this specific to this type of component. + fiber.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. + if (updatePayload !== null) { + return true; + } + return false; +} + +function prepareToHydrateHostTextInstance(fiber: Fiber): boolean { + if (!supportsHydration) { + invariant( + false, + 'Expected prepareToHydrateHostTextInstance() to never be called. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + } + + const textInstance: TextInstance = fiber.stateNode; + const textContent: string = fiber.memoizedProps; + const shouldUpdate = hydrateTextInstance(textInstance, textContent, fiber); + if (__DEV__) { + if (shouldUpdate) { + // We assume that prepareToHydrateHostTextInstance is called in a context where the + // hydration parent is the parent host component of this host text. + const returnFiber = hydrationParentFiber; + if (returnFiber !== null) { + switch (returnFiber.tag) { + case HostRoot: { + const parentContainer = returnFiber.stateNode.containerInfo; + didNotMatchHydratedContainerTextInstance( + parentContainer, + textInstance, + textContent, + ); + break; + } + case HostComponent: { + const parentType = returnFiber.type; + const parentProps = returnFiber.memoizedProps; + const parentInstance = returnFiber.stateNode; + didNotMatchHydratedTextInstance( + parentType, + parentProps, + parentInstance, + textInstance, + textContent, + ); + break; + } + } + } + } + } + return shouldUpdate; +} + +function prepareToHydrateHostSuspenseInstance(fiber: Fiber): void { + if (!supportsHydration) { + invariant( + false, + 'Expected prepareToHydrateHostSuspenseInstance() to never be called. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + } + + const suspenseState: null | SuspenseState = fiber.memoizedState; + const suspenseInstance: null | SuspenseInstance = + suspenseState !== null ? suspenseState.dehydrated : null; + invariant( + suspenseInstance, + 'Expected to have a hydrated suspense instance. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + hydrateSuspenseInstance(suspenseInstance, fiber); +} + +function skipPastDehydratedSuspenseInstance( + fiber: Fiber, +): null | HydratableInstance { + if (!supportsHydration) { + invariant( + false, + 'Expected skipPastDehydratedSuspenseInstance() to never be called. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + } + const suspenseState: null | SuspenseState = fiber.memoizedState; + const suspenseInstance: null | SuspenseInstance = + suspenseState !== null ? suspenseState.dehydrated : null; + invariant( + suspenseInstance, + 'Expected to have a hydrated suspense instance. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + return getNextHydratableInstanceAfterSuspenseInstance(suspenseInstance); +} + +function popToNextHostParent(fiber: Fiber): void { + let parent = fiber.return; + while ( + parent !== null && + parent.tag !== HostComponent && + parent.tag !== HostRoot && + parent.tag !== SuspenseComponent + ) { + parent = parent.return; + } + hydrationParentFiber = parent; +} + +function popHydrationState(fiber: Fiber): boolean { + if (!supportsHydration) { + return false; + } + if (fiber !== hydrationParentFiber) { + // We're deeper than the current hydration context, inside an inserted + // tree. + return false; + } + if (!isHydrating) { + // If we're not currently hydrating but we're in a hydration context, then + // we were an insertion and now need to pop up reenter hydration of our + // siblings. + popToNextHostParent(fiber); + isHydrating = true; + return false; + } + + const type = fiber.type; + + // If we have any remaining hydratable nodes, we need to delete them now. + // We only do this deeper than head and body since they tend to have random + // other nodes in them. We also ignore components with pure text content in + // side of them. + // TODO: Better heuristic. + if ( + fiber.tag !== HostComponent || + (type !== 'head' && + type !== 'body' && + !shouldSetTextContent(type, fiber.memoizedProps)) + ) { + let nextInstance = nextHydratableInstance; + while (nextInstance) { + deleteHydratableInstance(fiber, nextInstance); + nextInstance = getNextHydratableSibling(nextInstance); + } + } + + popToNextHostParent(fiber); + if (fiber.tag === SuspenseComponent) { + nextHydratableInstance = skipPastDehydratedSuspenseInstance(fiber); + } else { + nextHydratableInstance = hydrationParentFiber + ? getNextHydratableSibling(fiber.stateNode) + : null; + } + return true; +} + +function resetHydrationState(): void { + if (!supportsHydration) { + return; + } + + hydrationParentFiber = null; + nextHydratableInstance = null; + isHydrating = false; +} + +function getIsHydrating(): boolean { + return isHydrating; +} + +export { + warnIfHydrating, + enterHydrationState, + getIsHydrating, + reenterHydrationStateFromDehydratedSuspenseInstance, + resetHydrationState, + tryToClaimNextHydratableInstance, + prepareToHydrateHostInstance, + prepareToHydrateHostTextInstance, + prepareToHydrateHostSuspenseInstance, + popHydrationState, +}; diff --git a/packages/react-reconciler/src/ReactFiberLazyComponent.new.js b/packages/react-reconciler/src/ReactFiberLazyComponent.new.js new file mode 100644 index 0000000000000..3773f640439bf --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberLazyComponent.new.js @@ -0,0 +1,23 @@ +/** + * 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 + */ + +export function resolveDefaultProps(Component: any, baseProps: Object): Object { + if (Component && Component.defaultProps) { + // Resolve default props. Taken from ReactElement + const props = Object.assign({}, baseProps); + const defaultProps = Component.defaultProps; + for (const propName in defaultProps) { + if (props[propName] === undefined) { + props[propName] = defaultProps[propName]; + } + } + return props; + } + return baseProps; +} diff --git a/packages/react-reconciler/src/ReactFiberNewContext.new.js b/packages/react-reconciler/src/ReactFiberNewContext.new.js new file mode 100644 index 0000000000000..adf863706cb13 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberNewContext.new.js @@ -0,0 +1,389 @@ +/** + * 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 {ReactContext} from 'shared/ReactTypes'; +import type {Fiber, ContextDependency} from './ReactInternalTypes'; +import type {StackCursor} from './ReactFiberStack.new'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; + +import {isPrimaryRenderer} from './ReactFiberHostConfig'; +import {createCursor, push, pop} from './ReactFiberStack.new'; +import {MAX_SIGNED_31_BIT_INT} from './MaxInts'; +import { + ContextProvider, + ClassComponent, + DehydratedFragment, +} from './ReactWorkTags'; + +import invariant from 'shared/invariant'; +import is from 'shared/objectIs'; +import {createUpdate, enqueueUpdate, ForceUpdate} from './ReactUpdateQueue.new'; +import {NoWork} from './ReactFiberExpirationTime'; +import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork.new'; +import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags'; + +const valueCursor: StackCursor = createCursor(null); + +let rendererSigil; +if (__DEV__) { + // Use this to detect multiple renderers using the same context + rendererSigil = {}; +} + +let currentlyRenderingFiber: Fiber | null = null; +let lastContextDependency: ContextDependency | null = null; +let lastContextWithAllBitsObserved: ReactContext | null = null; + +let isDisallowedContextReadInDEV: boolean = false; + +export function resetContextDependencies(): void { + // This is called right before React yields execution, to ensure `readContext` + // cannot be called outside the render phase. + currentlyRenderingFiber = null; + lastContextDependency = null; + lastContextWithAllBitsObserved = null; + if (__DEV__) { + isDisallowedContextReadInDEV = false; + } +} + +export function enterDisallowedContextReadInDEV(): void { + if (__DEV__) { + isDisallowedContextReadInDEV = true; + } +} + +export function exitDisallowedContextReadInDEV(): void { + if (__DEV__) { + isDisallowedContextReadInDEV = false; + } +} + +export function pushProvider(providerFiber: Fiber, nextValue: T): void { + const context: ReactContext = providerFiber.type._context; + + if (isPrimaryRenderer) { + push(valueCursor, context._currentValue, providerFiber); + + context._currentValue = nextValue; + if (__DEV__) { + if ( + context._currentRenderer !== undefined && + context._currentRenderer !== null && + context._currentRenderer !== rendererSigil + ) { + console.error( + 'Detected multiple renderers concurrently rendering the ' + + 'same context provider. This is currently unsupported.', + ); + } + context._currentRenderer = rendererSigil; + } + } else { + push(valueCursor, context._currentValue2, providerFiber); + + context._currentValue2 = nextValue; + if (__DEV__) { + if ( + context._currentRenderer2 !== undefined && + context._currentRenderer2 !== null && + context._currentRenderer2 !== rendererSigil + ) { + console.error( + 'Detected multiple renderers concurrently rendering the ' + + 'same context provider. This is currently unsupported.', + ); + } + context._currentRenderer2 = rendererSigil; + } + } +} + +export function popProvider(providerFiber: Fiber): void { + const currentValue = valueCursor.current; + + pop(valueCursor, providerFiber); + + const context: ReactContext = providerFiber.type._context; + if (isPrimaryRenderer) { + context._currentValue = currentValue; + } else { + context._currentValue2 = currentValue; + } +} + +export function calculateChangedBits( + context: ReactContext, + newValue: T, + oldValue: T, +) { + if (is(oldValue, newValue)) { + // No change + return 0; + } else { + const changedBits = + typeof context._calculateChangedBits === 'function' + ? context._calculateChangedBits(oldValue, newValue) + : MAX_SIGNED_31_BIT_INT; + + if (__DEV__) { + if ((changedBits & MAX_SIGNED_31_BIT_INT) !== changedBits) { + console.error( + 'calculateChangedBits: Expected the return value to be a ' + + '31-bit integer. Instead received: %s', + changedBits, + ); + } + } + return changedBits | 0; + } +} + +export function scheduleWorkOnParentPath( + parent: Fiber | null, + renderExpirationTime: ExpirationTime, +) { + // Update the child expiration time of all the ancestors, including + // the alternates. + let node = parent; + while (node !== null) { + const alternate = node.alternate; + if (node.childExpirationTime < renderExpirationTime) { + node.childExpirationTime = renderExpirationTime; + if ( + alternate !== null && + alternate.childExpirationTime < renderExpirationTime + ) { + alternate.childExpirationTime = renderExpirationTime; + } + } else if ( + alternate !== null && + alternate.childExpirationTime < renderExpirationTime + ) { + alternate.childExpirationTime = renderExpirationTime; + } else { + // Neither alternate was updated, which means the rest of the + // ancestor path already has sufficient priority. + break; + } + node = node.return; + } +} + +export function propagateContextChange( + workInProgress: Fiber, + context: ReactContext, + changedBits: number, + renderExpirationTime: ExpirationTime, +): void { + let fiber = workInProgress.child; + if (fiber !== null) { + // Set the return pointer of the child to the work-in-progress fiber. + fiber.return = workInProgress; + } + while (fiber !== null) { + let nextFiber; + + // Visit this fiber. + const list = fiber.dependencies; + if (list !== null) { + nextFiber = fiber.child; + + let dependency = list.firstContext; + while (dependency !== null) { + // Check if the context matches. + if ( + dependency.context === context && + (dependency.observedBits & changedBits) !== 0 + ) { + // Match! Schedule an update on this fiber. + + if (fiber.tag === ClassComponent) { + // Schedule a force update on the work-in-progress. + const update = createUpdate(renderExpirationTime, null); + update.tag = ForceUpdate; + // TODO: Because we don't have a work-in-progress, this will add the + // update to the current fiber, too, which means it will persist even if + // this render is thrown away. Since it's a race condition, not sure it's + // worth fixing. + enqueueUpdate(fiber, update); + } + + if (fiber.expirationTime < renderExpirationTime) { + fiber.expirationTime = renderExpirationTime; + } + const alternate = fiber.alternate; + if ( + alternate !== null && + alternate.expirationTime < renderExpirationTime + ) { + alternate.expirationTime = renderExpirationTime; + } + + scheduleWorkOnParentPath(fiber.return, renderExpirationTime); + + // Mark the expiration time on the list, too. + if (list.expirationTime < renderExpirationTime) { + list.expirationTime = renderExpirationTime; + } + + // Since we already found a match, we can stop traversing the + // dependency list. + break; + } + dependency = dependency.next; + } + } else if (fiber.tag === ContextProvider) { + // Don't scan deeper if this is a matching provider + nextFiber = fiber.type === workInProgress.type ? null : fiber.child; + } else if ( + enableSuspenseServerRenderer && + fiber.tag === DehydratedFragment + ) { + // If a dehydrated suspense bounudary is in this subtree, we don't know + // if it will have any context consumers in it. The best we can do is + // mark it as having updates. + const parentSuspense = fiber.return; + invariant( + parentSuspense !== null, + 'We just came from a parent so we must have had a parent. This is a bug in React.', + ); + if (parentSuspense.expirationTime < renderExpirationTime) { + parentSuspense.expirationTime = renderExpirationTime; + } + const alternate = parentSuspense.alternate; + if ( + alternate !== null && + alternate.expirationTime < renderExpirationTime + ) { + alternate.expirationTime = renderExpirationTime; + } + // This is intentionally passing this fiber as the parent + // because we want to schedule this fiber as having work + // on its children. We'll use the childExpirationTime on + // this fiber to indicate that a context has changed. + scheduleWorkOnParentPath(parentSuspense, renderExpirationTime); + nextFiber = fiber.sibling; + } else { + // Traverse down. + nextFiber = fiber.child; + } + + if (nextFiber !== null) { + // Set the return pointer of the child to the work-in-progress fiber. + nextFiber.return = fiber; + } else { + // No child. Traverse to next sibling. + nextFiber = fiber; + while (nextFiber !== null) { + if (nextFiber === workInProgress) { + // We're back to the root of this subtree. Exit. + nextFiber = null; + break; + } + const sibling = nextFiber.sibling; + if (sibling !== null) { + // Set the return pointer of the sibling to the work-in-progress fiber. + sibling.return = nextFiber.return; + nextFiber = sibling; + break; + } + // No more siblings. Traverse up. + nextFiber = nextFiber.return; + } + } + fiber = nextFiber; + } +} + +export function prepareToReadContext( + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +): void { + currentlyRenderingFiber = workInProgress; + lastContextDependency = null; + lastContextWithAllBitsObserved = null; + + const dependencies = workInProgress.dependencies; + if (dependencies !== null) { + const firstContext = dependencies.firstContext; + if (firstContext !== null) { + if (dependencies.expirationTime >= renderExpirationTime) { + // Context list has a pending update. Mark that this fiber performed work. + markWorkInProgressReceivedUpdate(); + } + // Reset the work-in-progress list + dependencies.firstContext = null; + } + } +} + +export function readContext( + context: ReactContext, + observedBits: void | number | boolean, +): T { + if (__DEV__) { + // This warning would fire if you read context inside a Hook like useMemo. + // Unlike the class check below, it's not enforced in production for perf. + if (isDisallowedContextReadInDEV) { + console.error( + 'Context can only be read while React is rendering. ' + + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + + 'In function components, you can read it directly in the function body, but not ' + + 'inside Hooks like useReducer() or useMemo().', + ); + } + } + + if (lastContextWithAllBitsObserved === context) { + // Nothing to do. We already observe everything in this context. + } else if (observedBits === false || observedBits === 0) { + // Do not observe any updates. + } else { + let resolvedObservedBits; // Avoid deopting on observable arguments or heterogeneous types. + if ( + typeof observedBits !== 'number' || + observedBits === MAX_SIGNED_31_BIT_INT + ) { + // Observe all updates. + lastContextWithAllBitsObserved = ((context: any): ReactContext); + resolvedObservedBits = MAX_SIGNED_31_BIT_INT; + } else { + resolvedObservedBits = observedBits; + } + + const contextItem = { + context: ((context: any): ReactContext), + observedBits: resolvedObservedBits, + next: null, + }; + + if (lastContextDependency === null) { + invariant( + currentlyRenderingFiber !== null, + 'Context can only be read while React is rendering. ' + + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + + 'In function components, you can read it directly in the function body, but not ' + + 'inside Hooks like useReducer() or useMemo().', + ); + + // This is the first dependency for this component. Create a new list. + lastContextDependency = contextItem; + currentlyRenderingFiber.dependencies = { + expirationTime: NoWork, + firstContext: contextItem, + responders: null, + }; + } else { + // Append a new context item. + lastContextDependency = lastContextDependency.next = contextItem; + } + } + return isPrimaryRenderer ? context._currentValue : context._currentValue2; +} diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js new file mode 100644 index 0000000000000..1b80d50c480d6 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -0,0 +1,729 @@ +/** + * 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, SuspenseHydrationCallbacks} from './ReactInternalTypes'; +import type {FiberRoot} from './ReactInternalTypes'; +import type {RootTag} from './ReactRootTags'; +import type { + Instance, + TextInstance, + Container, + PublicInstance, +} from './ReactFiberHostConfig'; +import type {RendererInspectionConfig} from './ReactFiberHostConfig'; +import {FundamentalComponent} from './ReactWorkTags'; +import type {ReactNodeList, Thenable} from 'shared/ReactTypes'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; + +import { + findCurrentHostFiber, + findCurrentHostFiberWithNoPortals, +} from './ReactFiberTreeReflection'; +import {get as getInstance} from 'shared/ReactInstanceMap'; +import { + HostComponent, + ClassComponent, + HostRoot, + SuspenseComponent, +} from './ReactWorkTags'; +import getComponentName from 'shared/getComponentName'; +import invariant from 'shared/invariant'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; + +import {getPublicInstance} from './ReactFiberHostConfig'; +import { + findCurrentUnmaskedContext, + processChildContext, + emptyContextObject, + isContextProvider as isLegacyContextProvider, +} from './ReactFiberContext.new'; +import {createFiberRoot} from './ReactFiberRoot.new'; +import {injectInternals, onScheduleRoot} from './ReactFiberDevToolsHook.new'; +import { + requestCurrentTimeForUpdate, + computeExpirationForFiber, + scheduleUpdateOnFiber, + flushRoot, + batchedEventUpdates, + batchedUpdates, + unbatchedUpdates, + flushSync, + flushControlled, + deferredUpdates, + syncUpdates, + discreteUpdates, + flushDiscreteUpdates, + flushPassiveEffects, + warnIfNotScopedWithMatchingAct, + warnIfUnmockedScheduler, + IsThisRendererActing, +} from './ReactFiberWorkLoop.new'; +import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.new'; +import {getStackByFiberInDevAndProd} from './ReactFiberComponentStack'; +import { + isRendering as ReactCurrentFiberIsRendering, + current as ReactCurrentFiberCurrent, +} from './ReactCurrentFiber'; +import {StrictMode} from './ReactTypeOfMode'; +import { + Sync, + ContinuousHydration, + computeInteractiveExpiration, +} from './ReactFiberExpirationTime'; +import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig'; +import { + scheduleRefresh, + scheduleRoot, + setRefreshHandler, + findHostInstancesForRefresh, +} from './ReactFiberHotReloading.new'; + +// used by isTestEnvironment builds +import enqueueTask from 'shared/enqueueTask'; +import * as Scheduler from 'scheduler'; +// end isTestEnvironment imports + +export {createPortal} from './ReactPortal'; + +type OpaqueRoot = FiberRoot; + +// 0 is PROD, 1 is DEV. +// Might add PROFILE later. +type BundleType = 0 | 1; + +type DevToolsConfig = {| + bundleType: BundleType, + version: string, + rendererPackageName: string, + // Note: this actually *does* depend on Fiber internal fields. + // Used by "inspect clicked DOM element" in React DevTools. + findFiberByHostInstance?: (instance: Instance | TextInstance) => Fiber | null, + rendererConfig?: RendererInspectionConfig, +|}; + +let didWarnAboutNestedUpdates; +let didWarnAboutFindNodeInStrictMode; + +if (__DEV__) { + didWarnAboutNestedUpdates = false; + didWarnAboutFindNodeInStrictMode = {}; +} + +function getContextForSubtree( + parentComponent: ?React$Component, +): Object { + if (!parentComponent) { + return emptyContextObject; + } + + const fiber = getInstance(parentComponent); + const parentContext = findCurrentUnmaskedContext(fiber); + + if (fiber.tag === ClassComponent) { + const Component = fiber.type; + if (isLegacyContextProvider(Component)) { + return processChildContext(fiber, Component, parentContext); + } + } + + return parentContext; +} + +function findHostInstance(component: Object): PublicInstance | null { + const fiber = getInstance(component); + if (fiber === undefined) { + if (typeof component.render === 'function') { + invariant(false, 'Unable to find node on an unmounted component.'); + } else { + invariant( + false, + 'Argument appears to not be a ReactComponent. Keys: %s', + Object.keys(component), + ); + } + } + const hostFiber = findCurrentHostFiber(fiber); + if (hostFiber === null) { + return null; + } + return hostFiber.stateNode; +} + +function findHostInstanceWithWarning( + component: Object, + methodName: string, +): PublicInstance | null { + if (__DEV__) { + const fiber = getInstance(component); + if (fiber === undefined) { + if (typeof component.render === 'function') { + invariant(false, 'Unable to find node on an unmounted component.'); + } else { + invariant( + false, + 'Argument appears to not be a ReactComponent. Keys: %s', + Object.keys(component), + ); + } + } + const hostFiber = findCurrentHostFiber(fiber); + if (hostFiber === null) { + return null; + } + if (hostFiber.mode & StrictMode) { + const componentName = getComponentName(fiber.type) || 'Component'; + if (!didWarnAboutFindNodeInStrictMode[componentName]) { + didWarnAboutFindNodeInStrictMode[componentName] = true; + if (fiber.mode & StrictMode) { + console.error( + '%s is deprecated in StrictMode. ' + + '%s was passed an instance of %s which is inside StrictMode. ' + + 'Instead, add a ref directly to the element you want to reference. ' + + 'Learn more about using refs safely here: ' + + 'https://fb.me/react-strict-mode-find-node%s', + methodName, + methodName, + componentName, + getStackByFiberInDevAndProd(hostFiber), + ); + } else { + console.error( + '%s is deprecated in StrictMode. ' + + '%s was passed an instance of %s which renders StrictMode children. ' + + 'Instead, add a ref directly to the element you want to reference. ' + + 'Learn more about using refs safely here: ' + + 'https://fb.me/react-strict-mode-find-node%s', + methodName, + methodName, + componentName, + getStackByFiberInDevAndProd(hostFiber), + ); + } + } + } + return hostFiber.stateNode; + } + return findHostInstance(component); +} + +export function createContainer( + containerInfo: Container, + tag: RootTag, + hydrate: boolean, + hydrationCallbacks: null | SuspenseHydrationCallbacks, +): OpaqueRoot { + return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks); +} + +export function updateContainer( + element: ReactNodeList, + container: OpaqueRoot, + parentComponent: ?React$Component, + callback: ?Function, +): ExpirationTime { + if (__DEV__) { + onScheduleRoot(container, element); + } + const current = container.current; + const currentTime = requestCurrentTimeForUpdate(); + if (__DEV__) { + // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests + if ('undefined' !== typeof jest) { + warnIfUnmockedScheduler(current); + warnIfNotScopedWithMatchingAct(current); + } + } + const suspenseConfig = requestCurrentSuspenseConfig(); + const expirationTime = computeExpirationForFiber( + currentTime, + current, + suspenseConfig, + ); + + const context = getContextForSubtree(parentComponent); + if (container.context === null) { + container.context = context; + } else { + container.pendingContext = context; + } + + if (__DEV__) { + if ( + ReactCurrentFiberIsRendering && + ReactCurrentFiberCurrent !== null && + !didWarnAboutNestedUpdates + ) { + didWarnAboutNestedUpdates = true; + console.error( + 'Render methods should be a pure function of props and state; ' + + 'triggering nested component updates from render is not allowed. ' + + 'If necessary, trigger nested updates in componentDidUpdate.\n\n' + + 'Check the render method of %s.', + getComponentName(ReactCurrentFiberCurrent.type) || 'Unknown', + ); + } + } + + const update = createUpdate(expirationTime, suspenseConfig); + // Caution: React DevTools currently depends on this property + // being called "element". + update.payload = {element}; + + callback = callback === undefined ? null : callback; + if (callback !== null) { + if (__DEV__) { + if (typeof callback !== 'function') { + console.error( + 'render(...): Expected the last optional `callback` argument to be a ' + + 'function. Instead received: %s.', + callback, + ); + } + } + update.callback = callback; + } + + enqueueUpdate(current, update); + scheduleUpdateOnFiber(current, expirationTime); + + return expirationTime; +} + +export { + batchedEventUpdates, + batchedUpdates, + unbatchedUpdates, + deferredUpdates, + syncUpdates, + discreteUpdates, + flushDiscreteUpdates, + flushControlled, + flushSync, + flushPassiveEffects, + IsThisRendererActing, +}; + +export function getPublicRootInstance( + container: OpaqueRoot, +): React$Component | PublicInstance | null { + const containerFiber = container.current; + if (!containerFiber.child) { + return null; + } + switch (containerFiber.child.tag) { + case HostComponent: + return getPublicInstance(containerFiber.child.stateNode); + default: + return containerFiber.child.stateNode; + } +} + +export function attemptSynchronousHydration(fiber: Fiber): void { + switch (fiber.tag) { + case HostRoot: + const root: FiberRoot = fiber.stateNode; + if (root.hydrate) { + // Flush the first scheduled "update". + flushRoot(root, root.firstPendingTime); + } + break; + case SuspenseComponent: + flushSync(() => scheduleUpdateOnFiber(fiber, Sync)); + // If we're still blocked after this, we need to increase + // the priority of any promises resolving within this + // boundary so that they next attempt also has higher pri. + const retryExpTime = computeInteractiveExpiration( + requestCurrentTimeForUpdate(), + ); + markRetryTimeIfNotHydrated(fiber, retryExpTime); + break; + } +} + +function markRetryTimeImpl(fiber: Fiber, retryTime: ExpirationTime) { + const suspenseState: null | SuspenseState = fiber.memoizedState; + if (suspenseState !== null && suspenseState.dehydrated !== null) { + if (suspenseState.retryTime < retryTime) { + suspenseState.retryTime = retryTime; + } + } +} + +// Increases the priority of thennables when they resolve within this boundary. +function markRetryTimeIfNotHydrated(fiber: Fiber, retryTime: ExpirationTime) { + markRetryTimeImpl(fiber, retryTime); + const alternate = fiber.alternate; + if (alternate) { + markRetryTimeImpl(alternate, retryTime); + } +} + +export function attemptUserBlockingHydration(fiber: Fiber): void { + if (fiber.tag !== SuspenseComponent) { + // We ignore HostRoots here because we can't increase + // their priority and they should not suspend on I/O, + // since you have to wrap anything that might suspend in + // Suspense. + return; + } + const expTime = computeInteractiveExpiration(requestCurrentTimeForUpdate()); + scheduleUpdateOnFiber(fiber, expTime); + markRetryTimeIfNotHydrated(fiber, expTime); +} + +export function attemptContinuousHydration(fiber: Fiber): void { + if (fiber.tag !== SuspenseComponent) { + // We ignore HostRoots here because we can't increase + // their priority and they should not suspend on I/O, + // since you have to wrap anything that might suspend in + // Suspense. + return; + } + scheduleUpdateOnFiber(fiber, ContinuousHydration); + markRetryTimeIfNotHydrated(fiber, ContinuousHydration); +} + +export function attemptHydrationAtCurrentPriority(fiber: Fiber): void { + if (fiber.tag !== SuspenseComponent) { + // We ignore HostRoots here because we can't increase + // their priority other than synchronously flush it. + return; + } + const currentTime = requestCurrentTimeForUpdate(); + const expTime = computeExpirationForFiber(currentTime, fiber, null); + scheduleUpdateOnFiber(fiber, expTime); + markRetryTimeIfNotHydrated(fiber, expTime); +} + +export {findHostInstance}; + +export {findHostInstanceWithWarning}; + +export function findHostInstanceWithNoPortals( + fiber: Fiber, +): PublicInstance | null { + const hostFiber = findCurrentHostFiberWithNoPortals(fiber); + if (hostFiber === null) { + return null; + } + if (hostFiber.tag === FundamentalComponent) { + return hostFiber.stateNode.instance; + } + return hostFiber.stateNode; +} + +let shouldSuspendImpl = fiber => false; + +export function shouldSuspend(fiber: Fiber): boolean { + return shouldSuspendImpl(fiber); +} + +let overrideHookState = null; +let overrideProps = null; +let scheduleUpdate = null; +let setSuspenseHandler = null; + +if (__DEV__) { + const copyWithSetImpl = ( + obj: Object | Array, + path: Array, + idx: number, + value: any, + ) => { + if (idx >= path.length) { + return value; + } + const key = path[idx]; + const updated = Array.isArray(obj) ? obj.slice() : {...obj}; + // $FlowFixMe number or string is fine here + updated[key] = copyWithSetImpl(obj[key], path, idx + 1, value); + return updated; + }; + + const copyWithSet = ( + obj: Object | Array, + path: Array, + value: any, + ): Object | Array => { + return copyWithSetImpl(obj, path, 0, value); + }; + + // Support DevTools editable values for useState and useReducer. + overrideHookState = ( + fiber: Fiber, + id: number, + path: Array, + value: any, + ) => { + // For now, the "id" of stateful hooks is just the stateful hook index. + // This may change in the future with e.g. nested hooks. + let currentHook = fiber.memoizedState; + while (currentHook !== null && id > 0) { + currentHook = currentHook.next; + id--; + } + if (currentHook !== null) { + const newState = copyWithSet(currentHook.memoizedState, path, value); + currentHook.memoizedState = newState; + currentHook.baseState = newState; + + // We aren't actually adding an update to the queue, + // because there is no update we can add for useReducer hooks that won't trigger an error. + // (There's no appropriate action type for DevTools overrides.) + // As a result though, React will see the scheduled update as a noop and bailout. + // Shallow cloning props works as a workaround for now to bypass the bailout check. + fiber.memoizedProps = {...fiber.memoizedProps}; + + scheduleUpdateOnFiber(fiber, Sync); + } + }; + + // Support DevTools props for function components, forwardRef, memo, host components, etc. + overrideProps = (fiber: Fiber, path: Array, value: any) => { + fiber.pendingProps = copyWithSet(fiber.memoizedProps, path, value); + if (fiber.alternate) { + fiber.alternate.pendingProps = fiber.pendingProps; + } + scheduleUpdateOnFiber(fiber, Sync); + }; + + scheduleUpdate = (fiber: Fiber) => { + scheduleUpdateOnFiber(fiber, Sync); + }; + + setSuspenseHandler = (newShouldSuspendImpl: Fiber => boolean) => { + shouldSuspendImpl = newShouldSuspendImpl; + }; +} + +function findHostInstanceByFiber(fiber: Fiber): Instance | TextInstance | null { + const hostFiber = findCurrentHostFiber(fiber); + if (hostFiber === null) { + return null; + } + return hostFiber.stateNode; +} + +function emptyFindFiberByHostInstance( + instance: Instance | TextInstance, +): Fiber | null { + return null; +} + +function getCurrentFiberForDevTools() { + return ReactCurrentFiberCurrent; +} + +export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean { + const {findFiberByHostInstance} = devToolsConfig; + const {ReactCurrentDispatcher} = ReactSharedInternals; + + return injectInternals({ + bundleType: devToolsConfig.bundleType, + version: devToolsConfig.version, + rendererPackageName: devToolsConfig.rendererPackageName, + rendererConfig: devToolsConfig.rendererConfig, + overrideHookState, + overrideProps, + setSuspenseHandler, + scheduleUpdate, + currentDispatcherRef: ReactCurrentDispatcher, + findHostInstanceByFiber, + findFiberByHostInstance: + findFiberByHostInstance || emptyFindFiberByHostInstance, + // React Refresh + findHostInstancesForRefresh: __DEV__ ? findHostInstancesForRefresh : null, + scheduleRefresh: __DEV__ ? scheduleRefresh : null, + scheduleRoot: __DEV__ ? scheduleRoot : null, + setRefreshHandler: __DEV__ ? setRefreshHandler : null, + // Enables DevTools to append owner stacks to error messages in DEV mode. + getCurrentFiber: __DEV__ ? getCurrentFiberForDevTools : null, + }); +} + +const {IsSomeRendererActing} = ReactSharedInternals; +const isSchedulerMocked = + typeof Scheduler.unstable_flushAllWithoutAsserting === 'function'; +const flushWork = + Scheduler.unstable_flushAllWithoutAsserting || + function() { + let didFlushWork = false; + while (flushPassiveEffects()) { + didFlushWork = true; + } + + return didFlushWork; + }; + +function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) { + try { + flushWork(); + enqueueTask(() => { + if (flushWork()) { + flushWorkAndMicroTasks(onDone); + } else { + onDone(); + } + }); + } catch (err) { + onDone(err); + } +} + +// we track the 'depth' of the act() calls with this counter, +// so we can tell if any async act() calls try to run in parallel. + +let actingUpdatesScopeDepth = 0; +let didWarnAboutUsingActInProd = false; + +// eslint-disable-next-line no-inner-declarations +export function act(callback: () => Thenable): Thenable { + if (!__DEV__) { + if (didWarnAboutUsingActInProd === false) { + didWarnAboutUsingActInProd = true; + // eslint-disable-next-line react-internal/no-production-logging + console.error( + 'act(...) is not supported in production builds of React, and might not behave as expected.', + ); + } + } + + const previousActingUpdatesScopeDepth = actingUpdatesScopeDepth; + actingUpdatesScopeDepth++; + + const previousIsSomeRendererActing = IsSomeRendererActing.current; + const previousIsThisRendererActing = IsThisRendererActing.current; + IsSomeRendererActing.current = true; + IsThisRendererActing.current = true; + + function onDone() { + actingUpdatesScopeDepth--; + IsSomeRendererActing.current = previousIsSomeRendererActing; + IsThisRendererActing.current = previousIsThisRendererActing; + if (__DEV__) { + if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) { + // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned + console.error( + 'You seem to have overlapping act() calls, this is not supported. ' + + 'Be sure to await previous act() calls before making a new one. ', + ); + } + } + } + + let result; + try { + result = batchedUpdates(callback); + } catch (error) { + // on sync errors, we still want to 'cleanup' and decrement actingUpdatesScopeDepth + onDone(); + throw error; + } + + if ( + result !== null && + typeof result === 'object' && + typeof result.then === 'function' + ) { + // setup a boolean that gets set to true only + // once this act() call is await-ed + let called = false; + if (__DEV__) { + if (typeof Promise !== 'undefined') { + //eslint-disable-next-line no-undef + Promise.resolve() + .then(() => {}) + .then(() => { + if (called === false) { + console.error( + 'You called act(async () => ...) without await. ' + + 'This could lead to unexpected testing behaviour, interleaving multiple act ' + + 'calls and mixing their scopes. You should - await act(async () => ...);', + ); + } + }); + } + } + + // in the async case, the returned thenable runs the callback, flushes + // effects and microtasks in a loop until flushPassiveEffects() === false, + // and cleans up + return { + then(resolve, reject) { + called = true; + result.then( + () => { + if ( + actingUpdatesScopeDepth > 1 || + (isSchedulerMocked === true && + previousIsSomeRendererActing === true) + ) { + onDone(); + resolve(); + return; + } + // we're about to exit the act() scope, + // now's the time to flush tasks/effects + flushWorkAndMicroTasks((err: ?Error) => { + onDone(); + if (err) { + reject(err); + } else { + resolve(); + } + }); + }, + err => { + onDone(); + reject(err); + }, + ); + }, + }; + } else { + if (__DEV__) { + if (result !== undefined) { + console.error( + 'The callback passed to act(...) function ' + + 'must return undefined, or a Promise. You returned %s', + result, + ); + } + } + + // flush effects until none remain, and cleanup + try { + if ( + actingUpdatesScopeDepth === 1 && + (isSchedulerMocked === false || previousIsSomeRendererActing === false) + ) { + // we're about to exit the act() scope, + // now's the time to flush effects + flushWork(); + } + onDone(); + } catch (err) { + onDone(); + throw err; + } + + // in the sync case, the returned thenable only warns *if* await-ed + return { + then(resolve) { + if (__DEV__) { + console.error( + 'Do not await the result of calling act(...) with sync logic, it is not a Promise.', + ); + } + resolve(); + }, + }; + } +} diff --git a/packages/react-reconciler/src/ReactFiberRoot.new.js b/packages/react-reconciler/src/ReactFiberRoot.new.js new file mode 100644 index 0000000000000..35fff92dc2fca --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberRoot.new.js @@ -0,0 +1,197 @@ +/** + * 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 {FiberRoot, SuspenseHydrationCallbacks} from './ReactInternalTypes'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type {RootTag} from './ReactRootTags'; + +import {noTimeout} from './ReactFiberHostConfig'; +import {createHostRootFiber} from './ReactFiber.new'; +import {NoWork} from './ReactFiberExpirationTime'; +import { + enableSchedulerTracing, + enableSuspenseCallback, +} from 'shared/ReactFeatureFlags'; +import {unstable_getThreadID} from 'scheduler/tracing'; +import {NoPriority} from './SchedulerWithReactIntegration.new'; +import {initializeUpdateQueue} from './ReactUpdateQueue.new'; +import {clearPendingUpdates as clearPendingMutableSourceUpdates} from './ReactMutableSource.new'; + +function FiberRootNode(containerInfo, tag, hydrate) { + this.tag = tag; + this.current = null; + this.containerInfo = containerInfo; + this.pendingChildren = null; + this.pingCache = null; + this.finishedExpirationTime = NoWork; + this.finishedWork = null; + this.timeoutHandle = noTimeout; + this.context = null; + this.pendingContext = null; + this.hydrate = hydrate; + this.callbackNode = null; + this.callbackPriority = NoPriority; + this.firstPendingTime = NoWork; + this.lastPendingTime = NoWork; + this.firstSuspendedTime = NoWork; + this.lastSuspendedTime = NoWork; + this.nextKnownPendingLevel = NoWork; + this.lastPingedTime = NoWork; + this.lastExpiredTime = NoWork; + this.mutableSourceFirstPendingUpdateTime = NoWork; + this.mutableSourceLastPendingUpdateTime = NoWork; + + if (enableSchedulerTracing) { + this.interactionThreadID = unstable_getThreadID(); + this.memoizedInteractions = new Set(); + this.pendingInteractionMap = new Map(); + } + if (enableSuspenseCallback) { + this.hydrationCallbacks = null; + } +} + +export function createFiberRoot( + containerInfo: any, + tag: RootTag, + hydrate: boolean, + hydrationCallbacks: null | SuspenseHydrationCallbacks, +): FiberRoot { + const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any); + if (enableSuspenseCallback) { + root.hydrationCallbacks = hydrationCallbacks; + } + + // Cyclic construction. This cheats the type system right now because + // stateNode is any. + const uninitializedFiber = createHostRootFiber(tag); + root.current = uninitializedFiber; + uninitializedFiber.stateNode = root; + + initializeUpdateQueue(uninitializedFiber); + + return root; +} + +export function isRootSuspendedAtTime( + root: FiberRoot, + expirationTime: ExpirationTime, +): boolean { + const firstSuspendedTime = root.firstSuspendedTime; + const lastSuspendedTime = root.lastSuspendedTime; + return ( + firstSuspendedTime !== NoWork && + firstSuspendedTime >= expirationTime && + lastSuspendedTime <= expirationTime + ); +} + +export function markRootSuspendedAtTime( + root: FiberRoot, + expirationTime: ExpirationTime, +): void { + const firstSuspendedTime = root.firstSuspendedTime; + const lastSuspendedTime = root.lastSuspendedTime; + if (firstSuspendedTime < expirationTime) { + root.firstSuspendedTime = expirationTime; + } + if (lastSuspendedTime > expirationTime || firstSuspendedTime === NoWork) { + root.lastSuspendedTime = expirationTime; + } + + if (expirationTime <= root.lastPingedTime) { + root.lastPingedTime = NoWork; + } + + if (expirationTime <= root.lastExpiredTime) { + root.lastExpiredTime = NoWork; + } +} + +export function markRootUpdatedAtTime( + root: FiberRoot, + expirationTime: ExpirationTime, +): void { + // Update the range of pending times + const firstPendingTime = root.firstPendingTime; + if (expirationTime > firstPendingTime) { + root.firstPendingTime = expirationTime; + } + const lastPendingTime = root.lastPendingTime; + if (lastPendingTime === NoWork || expirationTime < lastPendingTime) { + root.lastPendingTime = expirationTime; + } + + // Update the range of suspended times. Treat everything lower priority or + // equal to this update as unsuspended. + const firstSuspendedTime = root.firstSuspendedTime; + if (firstSuspendedTime !== NoWork) { + if (expirationTime >= firstSuspendedTime) { + // The entire suspended range is now unsuspended. + root.firstSuspendedTime = root.lastSuspendedTime = root.nextKnownPendingLevel = NoWork; + } else if (expirationTime >= root.lastSuspendedTime) { + root.lastSuspendedTime = expirationTime + 1; + } + + // This is a pending level. Check if it's higher priority than the next + // known pending level. + if (expirationTime > root.nextKnownPendingLevel) { + root.nextKnownPendingLevel = expirationTime; + } + } +} + +export function markRootFinishedAtTime( + root: FiberRoot, + finishedExpirationTime: ExpirationTime, + remainingExpirationTime: ExpirationTime, +): void { + // Update the range of pending times + root.firstPendingTime = remainingExpirationTime; + if (remainingExpirationTime < root.lastPendingTime) { + // This usually means we've finished all the work, but it can also happen + // when something gets downprioritized during render, like a hidden tree. + root.lastPendingTime = remainingExpirationTime; + } + + // Update the range of suspended times. Treat everything higher priority or + // equal to this update as unsuspended. + if (finishedExpirationTime <= root.lastSuspendedTime) { + // The entire suspended range is now unsuspended. + root.firstSuspendedTime = root.lastSuspendedTime = root.nextKnownPendingLevel = NoWork; + } else if (finishedExpirationTime <= root.firstSuspendedTime) { + // Part of the suspended range is now unsuspended. Narrow the range to + // include everything between the unsuspended time (non-inclusive) and the + // last suspended time. + root.firstSuspendedTime = finishedExpirationTime - 1; + } + + if (finishedExpirationTime <= root.lastPingedTime) { + // Clear the pinged time + root.lastPingedTime = NoWork; + } + + if (finishedExpirationTime <= root.lastExpiredTime) { + // Clear the expired time + root.lastExpiredTime = NoWork; + } + + // Clear any pending updates that were just processed. + clearPendingMutableSourceUpdates(root, finishedExpirationTime); +} + +export function markRootExpiredAtTime( + root: FiberRoot, + expirationTime: ExpirationTime, +): void { + const lastExpiredTime = root.lastExpiredTime; + if (lastExpiredTime === NoWork || lastExpiredTime > expirationTime) { + root.lastExpiredTime = expirationTime; + } +} diff --git a/packages/react-reconciler/src/ReactFiberScope.new.js b/packages/react-reconciler/src/ReactFiberScope.new.js new file mode 100644 index 0000000000000..2bdd53285dae9 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberScope.new.js @@ -0,0 +1,200 @@ +/** + * 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 { + ReactScope, + ReactScopeInstance, + ReactScopeMethods, + ReactContext, + ReactScopeQuery, +} from 'shared/ReactTypes'; + +import {getPublicInstance, getInstanceFromNode} from './ReactFiberHostConfig'; + +import { + HostComponent, + SuspenseComponent, + ScopeComponent, + ContextProvider, +} from './ReactWorkTags'; +import {enableScopeAPI} from 'shared/ReactFeatureFlags'; + +function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean { + const memoizedState = fiber.memoizedState; + return ( + fiber.tag === SuspenseComponent && + memoizedState !== null && + memoizedState.dehydrated === null + ); +} + +function getSuspenseFallbackChild(fiber: Fiber): Fiber | null { + return ((((fiber.child: any): Fiber).sibling: any): Fiber).child; +} + +const emptyObject = {}; + +function collectScopedNodes( + node: Fiber, + fn: ReactScopeQuery, + scopedNodes: Array, +): void { + if (enableScopeAPI) { + if (node.tag === HostComponent) { + const {type, memoizedProps, stateNode} = node; + const instance = getPublicInstance(stateNode); + if ( + instance !== null && + fn(type, memoizedProps || emptyObject, instance) === true + ) { + scopedNodes.push(instance); + } + } + let child = node.child; + + if (isFiberSuspenseAndTimedOut(node)) { + child = getSuspenseFallbackChild(node); + } + if (child !== null) { + collectScopedNodesFromChildren(child, fn, scopedNodes); + } + } +} + +function collectFirstScopedNode( + node: Fiber, + fn: ReactScopeQuery, +): null | Object { + if (enableScopeAPI) { + if (node.tag === HostComponent) { + const {type, memoizedProps, stateNode} = node; + const instance = getPublicInstance(stateNode); + if (instance !== null && fn(type, memoizedProps, instance) === true) { + return instance; + } + } + let child = node.child; + + if (isFiberSuspenseAndTimedOut(node)) { + child = getSuspenseFallbackChild(node); + } + if (child !== null) { + return collectFirstScopedNodeFromChildren(child, fn); + } + } + return null; +} + +function collectScopedNodesFromChildren( + startingChild: Fiber, + fn: ReactScopeQuery, + scopedNodes: Array, +): void { + let child = startingChild; + while (child !== null) { + collectScopedNodes(child, fn, scopedNodes); + child = child.sibling; + } +} + +function collectFirstScopedNodeFromChildren( + startingChild: Fiber, + fn: ReactScopeQuery, +): Object | null { + let child = startingChild; + while (child !== null) { + const scopedNode = collectFirstScopedNode(child, fn); + if (scopedNode !== null) { + return scopedNode; + } + child = child.sibling; + } + return null; +} + +function collectNearestContextValues( + node: Fiber, + context: ReactContext, + childContextValues: Array, +): void { + if (node.tag === ContextProvider && node.type._context === context) { + const contextValue = node.memoizedProps.value; + childContextValues.push(contextValue); + } else { + let child = node.child; + + if (isFiberSuspenseAndTimedOut(node)) { + child = getSuspenseFallbackChild(node); + } + if (child !== null) { + collectNearestChildContextValues(child, context, childContextValues); + } + } +} + +function collectNearestChildContextValues( + startingChild: Fiber | null, + context: ReactContext, + childContextValues: Array, +): void { + let child = startingChild; + while (child !== null) { + collectNearestContextValues(child, context, childContextValues); + child = child.sibling; + } +} + +export function createScopeMethods( + scope: ReactScope, + instance: ReactScopeInstance, +): ReactScopeMethods { + return { + DO_NOT_USE_queryAllNodes(fn: ReactScopeQuery): null | Array { + const currentFiber = ((instance.fiber: any): Fiber); + const child = currentFiber.child; + const scopedNodes = []; + if (child !== null) { + collectScopedNodesFromChildren(child, fn, scopedNodes); + } + return scopedNodes.length === 0 ? null : scopedNodes; + }, + DO_NOT_USE_queryFirstNode(fn: ReactScopeQuery): null | Object { + const currentFiber = ((instance.fiber: any): Fiber); + const child = currentFiber.child; + if (child !== null) { + return collectFirstScopedNodeFromChildren(child, fn); + } + return null; + }, + containsNode(node: Object): boolean { + let fiber = getInstanceFromNode(node); + while (fiber !== null) { + if ( + fiber.tag === ScopeComponent && + fiber.type === scope && + fiber.stateNode === instance + ) { + return true; + } + fiber = fiber.return; + } + return false; + }, + getChildContextValues(context: ReactContext): Array { + const currentFiber = ((instance.fiber: any): Fiber); + const child = currentFiber.child; + const childContextValues = []; + if (child !== null) { + collectNearestChildContextValues(child, context, childContextValues); + } + return childContextValues; + }, + }; +} diff --git a/packages/react-reconciler/src/ReactFiberStack.new.js b/packages/react-reconciler/src/ReactFiberStack.new.js new file mode 100644 index 0000000000000..7cc4e330069ef --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberStack.new.js @@ -0,0 +1,97 @@ +/** + * 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'; + +export type StackCursor = {|current: T|}; + +const valueStack: Array = []; + +let fiberStack: Array; + +if (__DEV__) { + fiberStack = []; +} + +let index = -1; + +function createCursor(defaultValue: T): StackCursor { + return { + current: defaultValue, + }; +} + +function isEmpty(): boolean { + return index === -1; +} + +function pop(cursor: StackCursor, fiber: Fiber): void { + if (index < 0) { + if (__DEV__) { + console.error('Unexpected pop.'); + } + return; + } + + if (__DEV__) { + if (fiber !== fiberStack[index]) { + console.error('Unexpected Fiber popped.'); + } + } + + cursor.current = valueStack[index]; + + valueStack[index] = null; + + if (__DEV__) { + fiberStack[index] = null; + } + + index--; +} + +function push(cursor: StackCursor, value: T, fiber: Fiber): void { + index++; + + valueStack[index] = cursor.current; + + if (__DEV__) { + fiberStack[index] = fiber; + } + + cursor.current = value; +} + +function checkThatStackIsEmpty() { + if (__DEV__) { + if (index !== -1) { + console.error( + 'Expected an empty stack. Something was not reset properly.', + ); + } + } +} + +function resetStackAfterFatalErrorInDev() { + if (__DEV__) { + index = -1; + valueStack.length = 0; + fiberStack.length = 0; + } +} + +export { + createCursor, + isEmpty, + pop, + push, + // DEV only: + checkThatStackIsEmpty, + resetStackAfterFatalErrorInDev, +}; diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js new file mode 100644 index 0000000000000..94ebb2feb2484 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js @@ -0,0 +1,139 @@ +/** + * 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 {SuspenseInstance} from './ReactFiberHostConfig'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; +import {SuspenseComponent, SuspenseListComponent} from './ReactWorkTags'; +import {NoEffect, DidCapture} from './ReactSideEffectTags'; +import { + isSuspenseInstancePending, + isSuspenseInstanceFallback, +} from './ReactFiberHostConfig'; + +// A null SuspenseState represents an unsuspended normal Suspense boundary. +// A non-null SuspenseState means that it is blocked for one reason or another. +// - A non-null dehydrated field means it's blocked pending hydration. +// - A non-null dehydrated field can use isSuspenseInstancePending or +// isSuspenseInstanceFallback to query the reason for being dehydrated. +// - A null dehydrated field means it's blocked by something suspending and +// we're currently showing a fallback instead. +export type SuspenseState = {| + // If this boundary is still dehydrated, we store the SuspenseInstance + // here to indicate that it is dehydrated (flag) and for quick access + // to check things like isSuspenseInstancePending. + dehydrated: null | SuspenseInstance, + // Represents the work that was deprioritized when we committed the fallback. + // The work outside the boundary already committed at this level, so we cannot + // unhide the content without including it. + baseTime: ExpirationTime, + // Represents the earliest expiration time we should attempt to hydrate + // a dehydrated boundary at. + // Never is the default for dehydrated boundaries. + // NoWork is the default for normal boundaries, which turns into "normal" pri. + retryTime: ExpirationTime, +|}; + +export type SuspenseListTailMode = 'collapsed' | 'hidden' | void; + +export type SuspenseListRenderState = {| + isBackwards: boolean, + // The currently rendering tail row. + rendering: null | Fiber, + // The absolute time when we started rendering the tail row. + renderingStartTime: number, + // The last of the already rendered children. + last: null | Fiber, + // Remaining rows on the tail of the list. + tail: null | Fiber, + // The absolute time in ms that we'll expire the tail rendering. + tailExpiration: number, + // Tail insertions setting. + tailMode: SuspenseListTailMode, + // Last Effect before we rendered the "rendering" item. + // Used to remove new effects added by the rendered item. + lastEffect: null | Fiber, +|}; + +export function shouldCaptureSuspense( + workInProgress: Fiber, + hasInvisibleParent: boolean, +): boolean { + // If it was the primary children that just suspended, capture and render the + // fallback. Otherwise, don't capture and bubble to the next boundary. + const nextState: SuspenseState | null = workInProgress.memoizedState; + if (nextState !== null) { + if (nextState.dehydrated !== null) { + // A dehydrated boundary always captures. + return true; + } + return false; + } + const props = workInProgress.memoizedProps; + // In order to capture, the Suspense component must have a fallback prop. + if (props.fallback === undefined) { + return false; + } + // Regular boundaries always capture. + if (props.unstable_avoidThisFallback !== true) { + return true; + } + // If it's a boundary we should avoid, then we prefer to bubble up to the + // parent boundary if it is currently invisible. + if (hasInvisibleParent) { + return false; + } + // If the parent is not able to handle it, we must handle it. + return true; +} + +export function findFirstSuspended(row: Fiber): null | Fiber { + let node = row; + while (node !== null) { + if (node.tag === SuspenseComponent) { + const state: SuspenseState | null = node.memoizedState; + if (state !== null) { + const dehydrated: null | SuspenseInstance = state.dehydrated; + if ( + dehydrated === null || + isSuspenseInstancePending(dehydrated) || + isSuspenseInstanceFallback(dehydrated) + ) { + return node; + } + } + } else if ( + node.tag === SuspenseListComponent && + // revealOrder undefined can't be trusted because it don't + // keep track of whether it suspended or not. + node.memoizedProps.revealOrder !== undefined + ) { + const didSuspend = (node.effectTag & DidCapture) !== NoEffect; + if (didSuspend) { + return node; + } + } else if (node.child !== null) { + node.child.return = node; + node = node.child; + continue; + } + if (node === row) { + return null; + } + while (node.sibling === null) { + if (node.return === null || node.return === row) { + return null; + } + node = node.return; + } + node.sibling.return = node.return; + node = node.sibling; + } + return null; +} diff --git a/packages/react-reconciler/src/ReactFiberSuspenseContext.new.js b/packages/react-reconciler/src/ReactFiberSuspenseContext.new.js new file mode 100644 index 0000000000000..fd6892063091e --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberSuspenseContext.new.js @@ -0,0 +1,83 @@ +/** + * 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 {StackCursor} from './ReactFiberStack.new'; + +import {createCursor, push, pop} from './ReactFiberStack.new'; + +export opaque type SuspenseContext = number; +export opaque type SubtreeSuspenseContext: SuspenseContext = number; +export opaque type ShallowSuspenseContext: SuspenseContext = number; + +const DefaultSuspenseContext: SuspenseContext = 0b00; + +// The Suspense Context is split into two parts. The lower bits is +// inherited deeply down the subtree. The upper bits only affect +// this immediate suspense boundary and gets reset each new +// boundary or suspense list. +const SubtreeSuspenseContextMask: SuspenseContext = 0b01; + +// Subtree Flags: + +// InvisibleParentSuspenseContext indicates that one of our parent Suspense +// boundaries is not currently showing visible main content. +// Either because it is already showing a fallback or is not mounted at all. +// We can use this to determine if it is desirable to trigger a fallback at +// the parent. If not, then we might need to trigger undesirable boundaries +// and/or suspend the commit to avoid hiding the parent content. +export const InvisibleParentSuspenseContext: SubtreeSuspenseContext = 0b01; + +// Shallow Flags: + +// ForceSuspenseFallback can be used by SuspenseList to force newly added +// items into their fallback state during one of the render passes. +export const ForceSuspenseFallback: ShallowSuspenseContext = 0b10; + +export const suspenseStackCursor: StackCursor = createCursor( + DefaultSuspenseContext, +); + +export function hasSuspenseContext( + parentContext: SuspenseContext, + flag: SuspenseContext, +): boolean { + return (parentContext & flag) !== 0; +} + +export function setDefaultShallowSuspenseContext( + parentContext: SuspenseContext, +): SuspenseContext { + return parentContext & SubtreeSuspenseContextMask; +} + +export function setShallowSuspenseContext( + parentContext: SuspenseContext, + shallowContext: ShallowSuspenseContext, +): SuspenseContext { + return (parentContext & SubtreeSuspenseContextMask) | shallowContext; +} + +export function addSubtreeSuspenseContext( + parentContext: SuspenseContext, + subtreeContext: SubtreeSuspenseContext, +): SuspenseContext { + return parentContext | subtreeContext; +} + +export function pushSuspenseContext( + fiber: Fiber, + newContext: SuspenseContext, +): void { + push(suspenseStackCursor, newContext, fiber); +} + +export function popSuspenseContext(fiber: Fiber): void { + pop(suspenseStackCursor, fiber); +} diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js new file mode 100644 index 0000000000000..3e3586eed2f33 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberThrow.new.js @@ -0,0 +1,393 @@ +/** + * 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 {FiberRoot} from './ReactInternalTypes'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type {CapturedValue} from './ReactCapturedValue'; +import type {Update} from './ReactUpdateQueue.new'; +import type {Wakeable} from 'shared/ReactTypes'; +import type {SuspenseContext} from './ReactFiberSuspenseContext.new'; + +import getComponentName from 'shared/getComponentName'; +import { + ClassComponent, + HostRoot, + SuspenseComponent, + IncompleteClassComponent, +} from './ReactWorkTags'; +import { + DidCapture, + Incomplete, + NoEffect, + ShouldCapture, + LifecycleEffectMask, +} from './ReactSideEffectTags'; +import {NoMode, BlockingMode} from './ReactTypeOfMode'; +import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.new'; + +import {createCapturedValue} from './ReactCapturedValue'; +import { + enqueueCapturedUpdate, + createUpdate, + CaptureUpdate, + ForceUpdate, + enqueueUpdate, +} from './ReactUpdateQueue.new'; +import {getStackByFiberInDevAndProd} from './ReactFiberComponentStack'; +import {markFailedErrorBoundaryForHotReloading} from './ReactFiberHotReloading.new'; +import { + suspenseStackCursor, + InvisibleParentSuspenseContext, + hasSuspenseContext, +} from './ReactFiberSuspenseContext.new'; +import { + renderDidError, + onUncaughtError, + markLegacyErrorBoundaryAsFailed, + isAlreadyFailedLegacyErrorBoundary, + pingSuspendedRoot, +} from './ReactFiberWorkLoop.new'; +import {logCapturedError} from './ReactFiberErrorLogger'; + +import {Sync} from './ReactFiberExpirationTime'; + +const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; + +function createRootErrorUpdate( + fiber: Fiber, + errorInfo: CapturedValue, + expirationTime: ExpirationTime, +): Update { + const update = createUpdate(expirationTime, null); + // Unmount the root by rendering null. + update.tag = CaptureUpdate; + // Caution: React DevTools currently depends on this property + // being called "element". + update.payload = {element: null}; + const error = errorInfo.value; + update.callback = () => { + onUncaughtError(error); + logCapturedError(fiber, errorInfo); + }; + return update; +} + +function createClassErrorUpdate( + fiber: Fiber, + errorInfo: CapturedValue, + expirationTime: ExpirationTime, +): Update { + const update = createUpdate(expirationTime, null); + update.tag = CaptureUpdate; + const getDerivedStateFromError = fiber.type.getDerivedStateFromError; + if (typeof getDerivedStateFromError === 'function') { + const error = errorInfo.value; + update.payload = () => { + logCapturedError(fiber, errorInfo); + return getDerivedStateFromError(error); + }; + } + + const inst = fiber.stateNode; + if (inst !== null && typeof inst.componentDidCatch === 'function') { + update.callback = function callback() { + if (__DEV__) { + markFailedErrorBoundaryForHotReloading(fiber); + } + if (typeof getDerivedStateFromError !== 'function') { + // To preserve the preexisting retry behavior of error boundaries, + // we keep track of which ones already failed during this batch. + // This gets reset before we yield back to the browser. + // TODO: Warn in strict mode if getDerivedStateFromError is + // not defined. + markLegacyErrorBoundaryAsFailed(this); + + // Only log here if componentDidCatch is the only error boundary method defined + logCapturedError(fiber, errorInfo); + } + const error = errorInfo.value; + const stack = errorInfo.stack; + this.componentDidCatch(error, { + componentStack: stack !== null ? stack : '', + }); + if (__DEV__) { + if (typeof getDerivedStateFromError !== 'function') { + // If componentDidCatch is the only error boundary method defined, + // then it needs to call setState to recover from errors. + // If no state update is scheduled then the boundary will swallow the error. + if (fiber.expirationTime !== Sync) { + console.error( + '%s: Error boundaries should implement getDerivedStateFromError(). ' + + 'In that method, return a state update to display an error message or fallback UI.', + getComponentName(fiber.type) || 'Unknown', + ); + } + } + } + }; + } else if (__DEV__) { + update.callback = () => { + markFailedErrorBoundaryForHotReloading(fiber); + }; + } + return update; +} + +function attachPingListener( + root: FiberRoot, + renderExpirationTime: ExpirationTime, + wakeable: Wakeable, +) { + // Attach a listener to the promise to "ping" the root and retry. But + // only if one does not already exist for the current render expiration + // time (which acts like a "thread ID" here). + let pingCache = root.pingCache; + let threadIDs; + if (pingCache === null) { + pingCache = root.pingCache = new PossiblyWeakMap(); + threadIDs = new Set(); + pingCache.set(wakeable, threadIDs); + } else { + threadIDs = pingCache.get(wakeable); + if (threadIDs === undefined) { + threadIDs = new Set(); + pingCache.set(wakeable, threadIDs); + } + } + if (!threadIDs.has(renderExpirationTime)) { + // Memoize using the thread ID to prevent redundant listeners. + threadIDs.add(renderExpirationTime); + const ping = pingSuspendedRoot.bind( + null, + root, + wakeable, + renderExpirationTime, + ); + wakeable.then(ping, ping); + } +} + +function throwException( + root: FiberRoot, + returnFiber: Fiber, + sourceFiber: Fiber, + value: mixed, + renderExpirationTime: ExpirationTime, +) { + // The source fiber did not complete. + sourceFiber.effectTag |= Incomplete; + // Its effect list is no longer valid. + sourceFiber.firstEffect = sourceFiber.lastEffect = null; + + if ( + value !== null && + typeof value === 'object' && + typeof value.then === 'function' + ) { + // This is a wakeable. + const wakeable: Wakeable = (value: any); + + if ((sourceFiber.mode & BlockingMode) === NoMode) { + // Reset the memoizedState to what it was before we attempted + // to render it. + const currentSource = sourceFiber.alternate; + if (currentSource) { + sourceFiber.updateQueue = currentSource.updateQueue; + sourceFiber.memoizedState = currentSource.memoizedState; + sourceFiber.expirationTime = currentSource.expirationTime; + } else { + sourceFiber.updateQueue = null; + sourceFiber.memoizedState = null; + } + } + + const hasInvisibleParentBoundary = hasSuspenseContext( + suspenseStackCursor.current, + (InvisibleParentSuspenseContext: SuspenseContext), + ); + + // Schedule the nearest Suspense to re-render the timed out view. + let workInProgress = returnFiber; + do { + if ( + workInProgress.tag === SuspenseComponent && + shouldCaptureSuspense(workInProgress, hasInvisibleParentBoundary) + ) { + // Found the nearest boundary. + + // Stash the promise on the boundary fiber. If the boundary times out, we'll + // attach another listener to flip the boundary back to its normal state. + const wakeables: Set = (workInProgress.updateQueue: any); + if (wakeables === null) { + const updateQueue = (new Set(): any); + updateQueue.add(wakeable); + workInProgress.updateQueue = updateQueue; + } else { + wakeables.add(wakeable); + } + + // If the boundary is outside of blocking mode, we should *not* + // suspend the commit. Pretend as if the suspended component rendered + // null and keep rendering. In the commit phase, we'll schedule a + // subsequent synchronous update to re-render the Suspense. + // + // Note: It doesn't matter whether the component that suspended was + // inside a blocking mode tree. If the Suspense is outside of it, we + // should *not* suspend the commit. + if ((workInProgress.mode & BlockingMode) === NoMode) { + workInProgress.effectTag |= DidCapture; + + // We're going to commit this fiber even though it didn't complete. + // But we shouldn't call any lifecycle methods or callbacks. Remove + // all lifecycle effect tags. + sourceFiber.effectTag &= ~(LifecycleEffectMask | Incomplete); + + if (sourceFiber.tag === ClassComponent) { + const currentSourceFiber = sourceFiber.alternate; + if (currentSourceFiber === null) { + // This is a new mount. Change the tag so it's not mistaken for a + // completed class component. For example, we should not call + // componentWillUnmount if it is deleted. + sourceFiber.tag = IncompleteClassComponent; + } else { + // When we try rendering again, we should not reuse the current fiber, + // since it's known to be in an inconsistent state. Use a force update to + // prevent a bail out. + const update = createUpdate(Sync, null); + update.tag = ForceUpdate; + enqueueUpdate(sourceFiber, update); + } + } + + // The source fiber did not complete. Mark it with Sync priority to + // indicate that it still has pending work. + sourceFiber.expirationTime = Sync; + + // Exit without suspending. + return; + } + + // Confirmed that the boundary is in a concurrent mode tree. Continue + // with the normal suspend path. + // + // After this we'll use a set of heuristics to determine whether this + // render pass will run to completion or restart or "suspend" the commit. + // The actual logic for this is spread out in different places. + // + // This first principle is that if we're going to suspend when we complete + // a root, then we should also restart if we get an update or ping that + // might unsuspend it, and vice versa. The only reason to suspend is + // because you think you might want to restart before committing. However, + // it doesn't make sense to restart only while in the period we're suspended. + // + // Restarting too aggressively is also not good because it starves out any + // intermediate loading state. So we use heuristics to determine when. + + // Suspense Heuristics + // + // If nothing threw a Promise or all the same fallbacks are already showing, + // then don't suspend/restart. + // + // If this is an initial render of a new tree of Suspense boundaries and + // those trigger a fallback, then don't suspend/restart. We want to ensure + // that we can show the initial loading state as quickly as possible. + // + // If we hit a "Delayed" case, such as when we'd switch from content back into + // a fallback, then we should always suspend/restart. SuspenseConfig applies to + // this case. If none is defined, JND is used instead. + // + // If we're already showing a fallback and it gets "retried", allowing us to show + // another level, but there's still an inner boundary that would show a fallback, + // then we suspend/restart for 500ms since the last time we showed a fallback + // anywhere in the tree. This effectively throttles progressive loading into a + // consistent train of commits. This also gives us an opportunity to restart to + // get to the completed state slightly earlier. + // + // If there's ambiguity due to batching it's resolved in preference of: + // 1) "delayed", 2) "initial render", 3) "retry". + // + // We want to ensure that a "busy" state doesn't get force committed. We want to + // ensure that new initial loading states can commit as soon as possible. + + attachPingListener(root, renderExpirationTime, wakeable); + + workInProgress.effectTag |= ShouldCapture; + workInProgress.expirationTime = renderExpirationTime; + + return; + } + // This boundary already captured during this render. Continue to the next + // boundary. + workInProgress = workInProgress.return; + } while (workInProgress !== null); + // No boundary was found. Fallthrough to error mode. + // TODO: Use invariant so the message is stripped in prod? + value = new Error( + (getComponentName(sourceFiber.type) || 'A React component') + + ' suspended while rendering, but no fallback UI was specified.\n' + + '\n' + + 'Add a component higher in the tree to ' + + 'provide a loading indicator or placeholder to display.' + + getStackByFiberInDevAndProd(sourceFiber), + ); + } + + // We didn't find a boundary that could handle this type of exception. Start + // over and traverse parent path again, this time treating the exception + // as an error. + renderDidError(); + value = createCapturedValue(value, sourceFiber); + let workInProgress = returnFiber; + do { + switch (workInProgress.tag) { + case HostRoot: { + const errorInfo = value; + workInProgress.effectTag |= ShouldCapture; + workInProgress.expirationTime = renderExpirationTime; + const update = createRootErrorUpdate( + workInProgress, + errorInfo, + renderExpirationTime, + ); + enqueueCapturedUpdate(workInProgress, update); + return; + } + case ClassComponent: + // Capture and retry + const errorInfo = value; + const ctor = workInProgress.type; + const instance = workInProgress.stateNode; + if ( + (workInProgress.effectTag & DidCapture) === NoEffect && + (typeof ctor.getDerivedStateFromError === 'function' || + (instance !== null && + typeof instance.componentDidCatch === 'function' && + !isAlreadyFailedLegacyErrorBoundary(instance))) + ) { + workInProgress.effectTag |= ShouldCapture; + workInProgress.expirationTime = renderExpirationTime; + // Schedule the error boundary to re-render using updated state + const update = createClassErrorUpdate( + workInProgress, + errorInfo, + renderExpirationTime, + ); + enqueueCapturedUpdate(workInProgress, update); + return; + } + break; + default: + break; + } + workInProgress = workInProgress.return; + } while (workInProgress !== null); +} + +export {throwException, createRootErrorUpdate, createClassErrorUpdate}; diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js new file mode 100644 index 0000000000000..47c82b688c0c4 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js @@ -0,0 +1,149 @@ +/** + * 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'; +import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; + +import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new'; +import { + ClassComponent, + HostRoot, + HostComponent, + HostPortal, + ContextProvider, + SuspenseComponent, + SuspenseListComponent, +} from './ReactWorkTags'; +import {DidCapture, NoEffect, ShouldCapture} from './ReactSideEffectTags'; +import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags'; + +import {popHostContainer, popHostContext} from './ReactFiberHostContext.new'; +import {popSuspenseContext} from './ReactFiberSuspenseContext.new'; +import {resetHydrationState} from './ReactFiberHydrationContext.new'; +import { + isContextProvider as isLegacyContextProvider, + popContext as popLegacyContext, + popTopLevelContextObject as popTopLevelLegacyContextObject, +} from './ReactFiberContext.new'; +import {popProvider} from './ReactFiberNewContext.new'; + +import invariant from 'shared/invariant'; + +function unwindWork( + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +) { + switch (workInProgress.tag) { + case ClassComponent: { + const Component = workInProgress.type; + if (isLegacyContextProvider(Component)) { + popLegacyContext(workInProgress); + } + const effectTag = workInProgress.effectTag; + if (effectTag & ShouldCapture) { + workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; + return workInProgress; + } + return null; + } + case HostRoot: { + popHostContainer(workInProgress); + popTopLevelLegacyContextObject(workInProgress); + resetMutableSourceWorkInProgressVersions(); + const effectTag = workInProgress.effectTag; + invariant( + (effectTag & DidCapture) === NoEffect, + 'The root failed to unmount after an error. This is likely a bug in ' + + 'React. Please file an issue.', + ); + workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; + return workInProgress; + } + case HostComponent: { + // TODO: popHydrationState + popHostContext(workInProgress); + return null; + } + case SuspenseComponent: { + popSuspenseContext(workInProgress); + if (enableSuspenseServerRenderer) { + const suspenseState: null | SuspenseState = + workInProgress.memoizedState; + if (suspenseState !== null && suspenseState.dehydrated !== null) { + invariant( + workInProgress.alternate !== null, + 'Threw in newly mounted dehydrated component. This is likely a bug in ' + + 'React. Please file an issue.', + ); + resetHydrationState(); + } + } + const effectTag = workInProgress.effectTag; + if (effectTag & ShouldCapture) { + workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; + // Captured a suspense effect. Re-render the boundary. + return workInProgress; + } + return null; + } + case SuspenseListComponent: { + popSuspenseContext(workInProgress); + // SuspenseList doesn't actually catch anything. It should've been + // caught by a nested boundary. If not, it should bubble through. + return null; + } + case HostPortal: + popHostContainer(workInProgress); + return null; + case ContextProvider: + popProvider(workInProgress); + return null; + default: + return null; + } +} + +function unwindInterruptedWork(interruptedWork: Fiber) { + switch (interruptedWork.tag) { + case ClassComponent: { + const childContextTypes = interruptedWork.type.childContextTypes; + if (childContextTypes !== null && childContextTypes !== undefined) { + popLegacyContext(interruptedWork); + } + break; + } + case HostRoot: { + popHostContainer(interruptedWork); + popTopLevelLegacyContextObject(interruptedWork); + resetMutableSourceWorkInProgressVersions(); + break; + } + case HostComponent: { + popHostContext(interruptedWork); + break; + } + case HostPortal: + popHostContainer(interruptedWork); + break; + case SuspenseComponent: + popSuspenseContext(interruptedWork); + break; + case SuspenseListComponent: + popSuspenseContext(interruptedWork); + break; + case ContextProvider: + popProvider(interruptedWork); + break; + default: + break; + } +} + +export {unwindWork, unwindInterruptedWork}; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js new file mode 100644 index 0000000000000..9a1a90fef02e0 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -0,0 +1,3204 @@ +/** + * 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 {Wakeable} from 'shared/ReactTypes'; +import type {Fiber, FiberRoot} from './ReactInternalTypes'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type {ReactPriorityLevel} from './ReactInternalTypes'; +import type {Interaction} from 'scheduler/src/Tracing'; +import type {SuspenseConfig} from './ReactFiberSuspenseConfig'; +import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; +import type {Effect as HookEffect} from './ReactFiberHooks.new'; + +import { + warnAboutDeprecatedLifecycles, + deferPassiveEffectCleanupDuringUnmount, + runAllPassiveEffectDestroysBeforeCreates, + enableSuspenseServerRenderer, + replayFailedUnitOfWorkWithInvokeGuardedCallback, + enableProfilerTimer, + enableProfilerCommitHooks, + enableSchedulerTracing, + warnAboutUnmockedScheduler, + flushSuspenseFallbacksInTests, + disableSchedulerTimeoutBasedOnReactExpirationTime, +} from 'shared/ReactFeatureFlags'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; +import invariant from 'shared/invariant'; + +import { + scheduleCallback, + cancelCallback, + getCurrentPriorityLevel, + runWithPriority, + shouldYield, + requestPaint, + now, + NoPriority, + ImmediatePriority, + UserBlockingPriority, + NormalPriority, + LowPriority, + IdlePriority, + flushSyncCallbackQueue, + scheduleSyncCallback, +} from './SchedulerWithReactIntegration.new'; + +// The scheduler is imported here *only* to detect whether it's been mocked +import * as Scheduler from 'scheduler'; + +import {__interactionsRef, __subscriberRef} from 'scheduler/tracing'; + +import { + prepareForCommit, + resetAfterCommit, + scheduleTimeout, + cancelTimeout, + noTimeout, + warnsIfNotActing, +} from './ReactFiberHostConfig'; + +import { + createWorkInProgress, + assignFiberPropertiesInDEV, +} from './ReactFiber.new'; +import { + isRootSuspendedAtTime, + markRootSuspendedAtTime, + markRootFinishedAtTime, + markRootUpdatedAtTime, + markRootExpiredAtTime, +} from './ReactFiberRoot.new'; +import { + NoMode, + StrictMode, + ProfileMode, + BlockingMode, + ConcurrentMode, +} from './ReactTypeOfMode'; +import { + HostRoot, + ClassComponent, + SuspenseComponent, + SuspenseListComponent, + FunctionComponent, + ForwardRef, + MemoComponent, + SimpleMemoComponent, + Block, +} from './ReactWorkTags'; +import {LegacyRoot} from './ReactRootTags'; +import { + NoEffect, + PerformedWork, + Placement, + Update, + PlacementAndUpdate, + Deletion, + Ref, + ContentReset, + Snapshot, + Callback, + Passive, + Incomplete, + HostEffectMask, + Hydrating, + HydratingAndUpdate, +} from './ReactSideEffectTags'; +import { + NoWork, + Sync, + Never, + msToExpirationTime, + expirationTimeToMs, + computeInteractiveExpiration, + computeAsyncExpiration, + computeSuspenseExpiration, + inferPriorityFromExpirationTime, + LOW_PRIORITY_EXPIRATION, + Batched, + Idle, +} from './ReactFiberExpirationTime'; +import {beginWork as originalBeginWork} from './ReactFiberBeginWork.new'; +import {completeWork} from './ReactFiberCompleteWork.new'; +import {unwindWork, unwindInterruptedWork} from './ReactFiberUnwindWork.new'; +import { + throwException, + createRootErrorUpdate, + createClassErrorUpdate, +} from './ReactFiberThrow.new'; +import { + commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber, + commitLifeCycles as commitLayoutEffectOnFiber, + commitPassiveHookEffects, + commitPlacement, + commitWork, + commitDeletion, + commitDetachRef, + commitAttachRef, + commitPassiveEffectDurations, + commitResetTextContent, +} from './ReactFiberCommitWork.new'; +import {enqueueUpdate} from './ReactUpdateQueue.new'; +import {resetContextDependencies} from './ReactFiberNewContext.new'; +import { + resetHooksAfterThrow, + ContextOnlyDispatcher, + getIsUpdatingOpaqueValueInRenderPhaseInDEV, +} from './ReactFiberHooks.new'; +import {createCapturedValue} from './ReactCapturedValue'; + +import { + recordCommitTime, + recordPassiveEffectDuration, + startPassiveEffectTimer, + startProfilerTimer, + stopProfilerTimerIfRunningAndRecordDelta, +} from './ReactProfilerTimer.new'; + +// DEV stuff +import getComponentName from 'shared/getComponentName'; +import ReactStrictModeWarnings from './ReactStrictModeWarnings.new'; +import {getStackByFiberInDevAndProd} from './ReactFiberComponentStack'; +import { + isRendering as ReactCurrentDebugFiberIsRenderingInDEV, + resetCurrentFiber as resetCurrentDebugFiberInDEV, + setCurrentFiber as setCurrentDebugFiberInDEV, +} from './ReactCurrentFiber'; +import { + invokeGuardedCallback, + hasCaughtError, + clearCaughtError, +} from 'shared/ReactErrorUtils'; +import {onCommitRoot} from './ReactFiberDevToolsHook.new'; + +const ceil = Math.ceil; + +const { + ReactCurrentDispatcher, + ReactCurrentOwner, + IsSomeRendererActing, +} = ReactSharedInternals; + +type ExecutionContext = number; + +const NoContext = /* */ 0b000000; +const BatchedContext = /* */ 0b000001; +const EventContext = /* */ 0b000010; +const DiscreteEventContext = /* */ 0b000100; +const LegacyUnbatchedContext = /* */ 0b001000; +const RenderContext = /* */ 0b010000; +const CommitContext = /* */ 0b100000; + +type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5; +const RootIncomplete = 0; +const RootFatalErrored = 1; +const RootErrored = 2; +const RootSuspended = 3; +const RootSuspendedWithDelay = 4; +const RootCompleted = 5; + +// Describes where we are in the React execution stack +let executionContext: ExecutionContext = NoContext; +// The root we're working on +let workInProgressRoot: FiberRoot | null = null; +// The fiber we're working on +let workInProgress: Fiber | null = null; +// The expiration time we're rendering +let renderExpirationTime: ExpirationTime = NoWork; +// Whether to root completed, errored, suspended, etc. +let workInProgressRootExitStatus: RootExitStatus = RootIncomplete; +// A fatal error, if one is thrown +let workInProgressRootFatalError: mixed = null; +// Most recent event time among processed updates during this render. +// This is conceptually a time stamp but expressed in terms of an ExpirationTime +// because we deal mostly with expiration times in the hot path, so this avoids +// the conversion happening in the hot path. +let workInProgressRootLatestProcessedExpirationTime: ExpirationTime = Sync; +let workInProgressRootLatestSuspenseTimeout: ExpirationTime = Sync; +let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null; +// The work left over by components that were visited during this render. Only +// includes unprocessed updates, not work in bailed out children. +let workInProgressRootNextUnprocessedUpdateTime: ExpirationTime = NoWork; + +// If we're pinged while rendering we don't always restart immediately. +// This flag determines if it might be worthwhile to restart if an opportunity +// happens latere. +let workInProgressRootHasPendingPing: boolean = false; +// The most recent time we committed a fallback. This lets us ensure a train +// model where we don't commit new loading states in too quick succession. +let globalMostRecentFallbackTime: number = 0; +const FALLBACK_THROTTLE_MS: number = 500; + +let nextEffect: Fiber | null = null; +let hasUncaughtError = false; +let firstUncaughtError = null; +let legacyErrorBoundariesThatAlreadyFailed: Set | null = null; + +let rootDoesHavePassiveEffects: boolean = false; +let rootWithPendingPassiveEffects: FiberRoot | null = null; +let pendingPassiveEffectsRenderPriority: ReactPriorityLevel = NoPriority; +let pendingPassiveEffectsExpirationTime: ExpirationTime = NoWork; +let pendingPassiveHookEffectsMount: Array = []; +let pendingPassiveHookEffectsUnmount: Array = []; +let pendingPassiveProfilerEffects: Array = []; + +let rootsWithPendingDiscreteUpdates: Map< + FiberRoot, + ExpirationTime, +> | null = null; + +// Use these to prevent an infinite loop of nested updates +const NESTED_UPDATE_LIMIT = 50; +let nestedUpdateCount: number = 0; +let rootWithNestedUpdates: FiberRoot | null = null; + +const NESTED_PASSIVE_UPDATE_LIMIT = 50; +let nestedPassiveUpdateCount: number = 0; + +// Marks the need to reschedule pending interactions at these expiration times +// during the commit phase. This enables them to be traced across components +// that spawn new work during render. E.g. hidden boundaries, suspended SSR +// hydration or SuspenseList. +let spawnedWorkDuringRender: null | Array = null; + +// Expiration times are computed by adding to the current time (the start +// time). However, if two updates are scheduled within the same event, we +// should treat their start times as simultaneous, even if the actual clock +// time has advanced between the first and second call. + +// In other words, because expiration times determine how updates are batched, +// we want all updates of like priority that occur within the same event to +// receive the same expiration time. Otherwise we get tearing. +let currentEventTime: ExpirationTime = NoWork; + +// Dev only flag that tracks if passive effects are currently being flushed. +// We warn about state updates for unmounted components differently in this case. +let isFlushingPassiveEffects = false; + +export function getWorkInProgressRoot(): FiberRoot | null { + return workInProgressRoot; +} + +export function requestCurrentTimeForUpdate() { + if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { + // We're inside React, so it's fine to read the actual time. + return msToExpirationTime(now()); + } + // We're not inside React, so we may be in the middle of a browser event. + if (currentEventTime !== NoWork) { + // Use the same start time for all updates until we enter React again. + return currentEventTime; + } + // This is the first update since React yielded. Compute a new start time. + currentEventTime = msToExpirationTime(now()); + return currentEventTime; +} + +export function getCurrentTime() { + return msToExpirationTime(now()); +} + +export function computeExpirationForFiber( + currentTime: ExpirationTime, + fiber: Fiber, + suspenseConfig: null | SuspenseConfig, +): ExpirationTime { + const mode = fiber.mode; + if ((mode & BlockingMode) === NoMode) { + return Sync; + } + + const priorityLevel = getCurrentPriorityLevel(); + if ((mode & ConcurrentMode) === NoMode) { + return priorityLevel === ImmediatePriority ? Sync : Batched; + } + + if ((executionContext & RenderContext) !== NoContext) { + // Use whatever time we're already rendering + // TODO: Should there be a way to opt out, like with `runWithPriority`? + return renderExpirationTime; + } + + let expirationTime; + if (suspenseConfig !== null) { + // Compute an expiration time based on the Suspense timeout. + expirationTime = computeSuspenseExpiration( + currentTime, + suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION, + ); + } else { + // Compute an expiration time based on the Scheduler priority. + switch (priorityLevel) { + case ImmediatePriority: + expirationTime = Sync; + break; + case UserBlockingPriority: + // TODO: Rename this to computeUserBlockingExpiration + expirationTime = computeInteractiveExpiration(currentTime); + break; + case NormalPriority: + case LowPriority: // TODO: Handle LowPriority + // TODO: Rename this to... something better. + expirationTime = computeAsyncExpiration(currentTime); + break; + case IdlePriority: + expirationTime = Idle; + break; + default: + invariant(false, 'Expected a valid priority level'); + } + } + + // If we're in the middle of rendering a tree, do not update at the same + // expiration time that is already rendering. + // TODO: We shouldn't have to do this if the update is on a different root. + // Refactor computeExpirationForFiber + scheduleUpdate so we have access to + // the root when we check for this condition. + if (workInProgressRoot !== null && expirationTime === renderExpirationTime) { + // This is a trick to move this update into a separate batch + expirationTime -= 1; + } + + return expirationTime; +} + +export function scheduleUpdateOnFiber( + fiber: Fiber, + expirationTime: ExpirationTime, +) { + checkForNestedUpdates(); + warnAboutRenderPhaseUpdatesInDEV(fiber); + + const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime); + if (root === null) { + warnAboutUpdateOnUnmountedFiberInDEV(fiber); + return; + } + + // TODO: computeExpirationForFiber also reads the priority. Pass the + // priority as an argument to that function and this one. + const priorityLevel = getCurrentPriorityLevel(); + + if (expirationTime === Sync) { + if ( + // Check if we're inside unbatchedUpdates + (executionContext & LegacyUnbatchedContext) !== NoContext && + // Check if we're not already rendering + (executionContext & (RenderContext | CommitContext)) === NoContext + ) { + // Register pending interactions on the root to avoid losing traced interaction data. + schedulePendingInteractions(root, expirationTime); + + // This is a legacy edge case. The initial mount of a ReactDOM.render-ed + // root inside of batchedUpdates should be synchronous, but layout updates + // should be deferred until the end of the batch. + performSyncWorkOnRoot(root); + } else { + ensureRootIsScheduled(root); + schedulePendingInteractions(root, expirationTime); + if (executionContext === NoContext) { + // Flush the synchronous work now, unless we're already working or inside + // a batch. This is intentionally inside scheduleUpdateOnFiber instead of + // scheduleCallbackForFiber to preserve the ability to schedule a callback + // without immediately flushing it. We only do this for user-initiated + // updates, to preserve historical behavior of legacy mode. + flushSyncCallbackQueue(); + } + } + } else { + ensureRootIsScheduled(root); + schedulePendingInteractions(root, expirationTime); + } + + if ( + (executionContext & DiscreteEventContext) !== NoContext && + // Only updates at user-blocking priority or greater are considered + // discrete, even inside a discrete event. + (priorityLevel === UserBlockingPriority || + priorityLevel === ImmediatePriority) + ) { + // This is the result of a discrete event. Track the lowest priority + // discrete update per root so we can flush them early, if needed. + if (rootsWithPendingDiscreteUpdates === null) { + rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]); + } else { + const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root); + if (lastDiscreteTime === undefined || lastDiscreteTime > expirationTime) { + rootsWithPendingDiscreteUpdates.set(root, expirationTime); + } + } + } +} + +// This is split into a separate function so we can mark a fiber with pending +// work without treating it as a typical update that originates from an event; +// e.g. retrying a Suspense boundary isn't an update, but it does schedule work +// on a fiber. +function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { + // Update the source fiber's expiration time + if (fiber.expirationTime < expirationTime) { + fiber.expirationTime = expirationTime; + } + let alternate = fiber.alternate; + if (alternate !== null && alternate.expirationTime < expirationTime) { + alternate.expirationTime = expirationTime; + } + // Walk the parent path to the root and update the child expiration time. + let node = fiber.return; + let root = null; + if (node === null && fiber.tag === HostRoot) { + root = fiber.stateNode; + } else { + while (node !== null) { + alternate = node.alternate; + if (node.childExpirationTime < expirationTime) { + node.childExpirationTime = expirationTime; + if ( + alternate !== null && + alternate.childExpirationTime < expirationTime + ) { + alternate.childExpirationTime = expirationTime; + } + } else if ( + alternate !== null && + alternate.childExpirationTime < expirationTime + ) { + alternate.childExpirationTime = expirationTime; + } + if (node.return === null && node.tag === HostRoot) { + root = node.stateNode; + break; + } + node = node.return; + } + } + + if (root !== null) { + if (workInProgressRoot === root) { + // Received an update to a tree that's in the middle of rendering. Mark + // that's unprocessed work on this root. + markUnprocessedUpdateTime(expirationTime); + + if (workInProgressRootExitStatus === RootSuspendedWithDelay) { + // The root already suspended with a delay, which means this render + // definitely won't finish. Since we have a new update, let's mark it as + // suspended now, right before marking the incoming update. This has the + // effect of interrupting the current render and switching to the update. + // TODO: This happens to work when receiving an update during the render + // phase, because of the trick inside computeExpirationForFiber to + // subtract 1 from `renderExpirationTime` to move it into a + // separate bucket. But we should probably model it with an exception, + // using the same mechanism we use to force hydration of a subtree. + // TODO: This does not account for low pri updates that were already + // scheduled before the root started rendering. Need to track the next + // pending expiration time (perhaps by backtracking the return path) and + // then trigger a restart in the `renderDidSuspendDelayIfPossible` path. + markRootSuspendedAtTime(root, renderExpirationTime); + } + } + // Mark that the root has a pending update. + markRootUpdatedAtTime(root, expirationTime); + } + + return root; +} + +function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime { + // Determines the next expiration time that the root should render, taking + // into account levels that may be suspended, or levels that may have + // received a ping. + + const lastExpiredTime = root.lastExpiredTime; + if (lastExpiredTime !== NoWork) { + return lastExpiredTime; + } + + // "Pending" refers to any update that hasn't committed yet, including if it + // suspended. The "suspended" range is therefore a subset. + const firstPendingTime = root.firstPendingTime; + if (!isRootSuspendedAtTime(root, firstPendingTime)) { + // The highest priority pending time is not suspended. Let's work on that. + return firstPendingTime; + } + + // If the first pending time is suspended, check if there's a lower priority + // pending level that we know about. Or check if we received a ping. Work + // on whichever is higher priority. + const lastPingedTime = root.lastPingedTime; + const nextKnownPendingLevel = root.nextKnownPendingLevel; + const nextLevel = + lastPingedTime > nextKnownPendingLevel + ? lastPingedTime + : nextKnownPendingLevel; + if (nextLevel <= Idle && firstPendingTime !== nextLevel) { + // Don't work on Idle/Never priority unless everything else is committed. + return NoWork; + } + return nextLevel; +} + +// Use this function to schedule a task for a root. There's only one task per +// root; if a task was already scheduled, we'll check to make sure the +// expiration time of the existing task is the same as the expiration time of +// the next level that the root has work on. This function is called on every +// update, and right before exiting a task. +function ensureRootIsScheduled(root: FiberRoot) { + const lastExpiredTime = root.lastExpiredTime; + if (lastExpiredTime !== NoWork) { + // Special case: Expired work should flush synchronously. + root.callbackExpirationTime = Sync; + root.callbackPriority = ImmediatePriority; + root.callbackNode = scheduleSyncCallback( + performSyncWorkOnRoot.bind(null, root), + ); + return; + } + + const expirationTime = getNextRootExpirationTimeToWorkOn(root); + const existingCallbackNode = root.callbackNode; + if (expirationTime === NoWork) { + // There's nothing to work on. + if (existingCallbackNode !== null) { + root.callbackNode = null; + root.callbackExpirationTime = NoWork; + root.callbackPriority = NoPriority; + } + return; + } + + // TODO: If this is an update, we already read the current time. Pass the + // time as an argument. + const currentTime = requestCurrentTimeForUpdate(); + const priorityLevel = inferPriorityFromExpirationTime( + currentTime, + expirationTime, + ); + + // If there's an existing render task, confirm it has the correct priority and + // expiration time. Otherwise, we'll cancel it and schedule a new one. + if (existingCallbackNode !== null) { + const existingCallbackPriority = root.callbackPriority; + const existingCallbackExpirationTime = root.callbackExpirationTime; + if ( + // Callback must have the exact same expiration time. + existingCallbackExpirationTime === expirationTime && + // Callback must have greater or equal priority. + existingCallbackPriority >= priorityLevel + ) { + // Existing callback is sufficient. + return; + } + // Need to schedule a new task. + // TODO: Instead of scheduling a new task, we should be able to change the + // priority of the existing one. + cancelCallback(existingCallbackNode); + } + + root.callbackExpirationTime = expirationTime; + root.callbackPriority = priorityLevel; + + let callbackNode; + if (expirationTime === Sync) { + // Sync React callbacks are scheduled on a special internal queue + callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root)); + } else if (disableSchedulerTimeoutBasedOnReactExpirationTime) { + callbackNode = scheduleCallback( + priorityLevel, + performConcurrentWorkOnRoot.bind(null, root), + ); + } else { + callbackNode = scheduleCallback( + priorityLevel, + performConcurrentWorkOnRoot.bind(null, root), + // Compute a task timeout based on the expiration time. This also affects + // ordering because tasks are processed in timeout order. + {timeout: expirationTimeToMs(expirationTime) - now()}, + ); + } + + root.callbackNode = callbackNode; +} + +// This is the entry point for every concurrent task, i.e. anything that +// goes through Scheduler. +function performConcurrentWorkOnRoot(root, didTimeout) { + // Since we know we're in a React event, we can clear the current + // event time. The next update will compute a new event time. + currentEventTime = NoWork; + + // Check if the render expired. + if (didTimeout) { + // The render task took too long to complete. Mark the current time as + // expired to synchronously render all expired work in a single batch. + const currentTime = requestCurrentTimeForUpdate(); + markRootExpiredAtTime(root, currentTime); + // This will schedule a synchronous callback. + ensureRootIsScheduled(root); + return null; + } + + // Determine the next expiration time to work on, using the fields stored + // on the root. + let expirationTime = getNextRootExpirationTimeToWorkOn(root); + if (expirationTime === NoWork) { + return null; + } + const originalCallbackNode = root.callbackNode; + invariant( + (executionContext & (RenderContext | CommitContext)) === NoContext, + 'Should not already be working.', + ); + + flushPassiveEffects(); + + let exitStatus = renderRootConcurrent(root, expirationTime); + + if (exitStatus !== RootIncomplete) { + if (exitStatus === RootErrored) { + // If something threw an error, try rendering one more time. We'll + // render synchronously to block concurrent data mutations, and we'll + // render at Idle (or lower) so that all pending updates are included. + // If it still fails after the second attempt, we'll give up and commit + // the resulting tree. + expirationTime = expirationTime > Idle ? Idle : expirationTime; + exitStatus = renderRootSync(root, expirationTime); + } + + if (exitStatus === RootFatalErrored) { + const fatalError = workInProgressRootFatalError; + prepareFreshStack(root, expirationTime); + markRootSuspendedAtTime(root, expirationTime); + ensureRootIsScheduled(root); + throw fatalError; + } + + // We now have a consistent tree. The next step is either to commit it, + // or, if something suspended, wait to commit it after a timeout. + const finishedWork: Fiber = (root.current.alternate: any); + root.finishedWork = finishedWork; + root.finishedExpirationTime = expirationTime; + root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork); + finishConcurrentRender(root, finishedWork, exitStatus, expirationTime); + } + + ensureRootIsScheduled(root); + if (root.callbackNode === originalCallbackNode) { + // The task node scheduled for this root is the same one that's + // currently executed. Need to return a continuation. + return performConcurrentWorkOnRoot.bind(null, root); + } + return null; +} + +function finishConcurrentRender( + root, + finishedWork, + exitStatus, + expirationTime, +) { + switch (exitStatus) { + case RootIncomplete: + case RootFatalErrored: { + invariant(false, 'Root did not complete. This is a bug in React.'); + } + // Flow knows about invariant, so it complains if I add a break + // statement, but eslint doesn't know about invariant, so it complains + // if I do. eslint-disable-next-line no-fallthrough + case RootErrored: { + // We should have already attempted to retry this tree. If we reached + // this point, it errored again. Commit it. + commitRoot(root); + break; + } + case RootSuspended: { + markRootSuspendedAtTime(root, expirationTime); + const lastSuspendedTime = root.lastSuspendedTime; + + // We have an acceptable loading state. We need to figure out if we + // should immediately commit it or wait a bit. + + // If we have processed new updates during this render, we may now + // have a new loading state ready. We want to ensure that we commit + // that as soon as possible. + const hasNotProcessedNewUpdates = + workInProgressRootLatestProcessedExpirationTime === Sync; + if ( + hasNotProcessedNewUpdates && + // do not delay if we're inside an act() scope + !( + __DEV__ && + flushSuspenseFallbacksInTests && + IsThisRendererActing.current + ) + ) { + // If we have not processed any new updates during this pass, then + // this is either a retry of an existing fallback state or a + // hidden tree. Hidden trees shouldn't be batched with other work + // and after that's fixed it can only be a retry. We're going to + // throttle committing retries so that we don't show too many + // loading states too quickly. + const msUntilTimeout = + globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - now(); + // Don't bother with a very short suspense time. + if (msUntilTimeout > 10) { + if (workInProgressRootHasPendingPing) { + const lastPingedTime = root.lastPingedTime; + if (lastPingedTime === NoWork || lastPingedTime >= expirationTime) { + // This render was pinged but we didn't get to restart + // earlier so try restarting now instead. + root.lastPingedTime = expirationTime; + prepareFreshStack(root, expirationTime); + break; + } + } + + const nextTime = getNextRootExpirationTimeToWorkOn(root); + if (nextTime !== NoWork && nextTime !== expirationTime) { + // There's additional work on this root. + break; + } + if ( + lastSuspendedTime !== NoWork && + lastSuspendedTime !== expirationTime + ) { + // We should prefer to render the fallback of at the last + // suspended level. Ping the last suspended level to try + // rendering it again. + root.lastPingedTime = lastSuspendedTime; + break; + } + + // The render is suspended, it hasn't timed out, and there's no + // lower priority work to do. Instead of committing the fallback + // immediately, wait for more data to arrive. + root.timeoutHandle = scheduleTimeout( + commitRoot.bind(null, root), + msUntilTimeout, + ); + break; + } + } + // The work expired. Commit immediately. + commitRoot(root); + break; + } + case RootSuspendedWithDelay: { + markRootSuspendedAtTime(root, expirationTime); + const lastSuspendedTime = root.lastSuspendedTime; + + if ( + // do not delay if we're inside an act() scope + !( + __DEV__ && + flushSuspenseFallbacksInTests && + IsThisRendererActing.current + ) + ) { + // We're suspended in a state that should be avoided. We'll try to + // avoid committing it for as long as the timeouts let us. + if (workInProgressRootHasPendingPing) { + const lastPingedTime = root.lastPingedTime; + if (lastPingedTime === NoWork || lastPingedTime >= expirationTime) { + // This render was pinged but we didn't get to restart earlier + // so try restarting now instead. + root.lastPingedTime = expirationTime; + prepareFreshStack(root, expirationTime); + break; + } + } + + const nextTime = getNextRootExpirationTimeToWorkOn(root); + if (nextTime !== NoWork && nextTime !== expirationTime) { + // There's additional work on this root. + break; + } + if ( + lastSuspendedTime !== NoWork && + lastSuspendedTime !== expirationTime + ) { + // We should prefer to render the fallback of at the last + // suspended level. Ping the last suspended level to try + // rendering it again. + root.lastPingedTime = lastSuspendedTime; + break; + } + + let msUntilTimeout; + if (workInProgressRootLatestSuspenseTimeout !== Sync) { + // We have processed a suspense config whose expiration time we + // can use as the timeout. + msUntilTimeout = + expirationTimeToMs(workInProgressRootLatestSuspenseTimeout) - now(); + } else if (workInProgressRootLatestProcessedExpirationTime === Sync) { + // This should never normally happen because only new updates + // cause delayed states, so we should have processed something. + // However, this could also happen in an offscreen tree. + msUntilTimeout = 0; + } else { + // If we don't have a suspense config, we're going to use a + // heuristic to determine how long we can suspend. + const eventTimeMs: number = inferTimeFromExpirationTime( + workInProgressRootLatestProcessedExpirationTime, + ); + const currentTimeMs = now(); + const timeUntilExpirationMs = + expirationTimeToMs(expirationTime) - currentTimeMs; + let timeElapsed = currentTimeMs - eventTimeMs; + if (timeElapsed < 0) { + // We get this wrong some time since we estimate the time. + timeElapsed = 0; + } + + msUntilTimeout = jnd(timeElapsed) - timeElapsed; + + // Clamp the timeout to the expiration time. TODO: Once the + // event time is exact instead of inferred from expiration time + // we don't need this. + if (timeUntilExpirationMs < msUntilTimeout) { + msUntilTimeout = timeUntilExpirationMs; + } + } + + // Don't bother with a very short suspense time. + if (msUntilTimeout > 10) { + // The render is suspended, it hasn't timed out, and there's no + // lower priority work to do. Instead of committing the fallback + // immediately, wait for more data to arrive. + root.timeoutHandle = scheduleTimeout( + commitRoot.bind(null, root), + msUntilTimeout, + ); + break; + } + } + // The work expired. Commit immediately. + commitRoot(root); + break; + } + case RootCompleted: { + // The work completed. Ready to commit. + if ( + // do not delay if we're inside an act() scope + !( + __DEV__ && + flushSuspenseFallbacksInTests && + IsThisRendererActing.current + ) && + workInProgressRootLatestProcessedExpirationTime !== Sync && + workInProgressRootCanSuspendUsingConfig !== null + ) { + // If we have exceeded the minimum loading delay, which probably + // means we have shown a spinner already, we might have to suspend + // a bit longer to ensure that the spinner is shown for + // enough time. + const msUntilTimeout = computeMsUntilSuspenseLoadingDelay( + workInProgressRootLatestProcessedExpirationTime, + expirationTime, + workInProgressRootCanSuspendUsingConfig, + ); + if (msUntilTimeout > 10) { + markRootSuspendedAtTime(root, expirationTime); + root.timeoutHandle = scheduleTimeout( + commitRoot.bind(null, root), + msUntilTimeout, + ); + break; + } + } + commitRoot(root); + break; + } + default: { + invariant(false, 'Unknown root exit status.'); + } + } +} + +// This is the entry point for synchronous tasks that don't go +// through Scheduler +function performSyncWorkOnRoot(root) { + invariant( + (executionContext & (RenderContext | CommitContext)) === NoContext, + 'Should not already be working.', + ); + + flushPassiveEffects(); + + const lastExpiredTime = root.lastExpiredTime; + + let expirationTime; + if (lastExpiredTime !== NoWork) { + // There's expired work on this root. Check if we have a partial tree + // that we can reuse. + if ( + root === workInProgressRoot && + renderExpirationTime >= lastExpiredTime + ) { + // There's a partial tree with equal or greater than priority than the + // expired level. Finish rendering it before rendering the rest of the + // expired work. + expirationTime = renderExpirationTime; + } else { + // Start a fresh tree. + expirationTime = lastExpiredTime; + } + } else { + // There's no expired work. This must be a new, synchronous render. + expirationTime = Sync; + } + + let exitStatus = renderRootSync(root, expirationTime); + + if (root.tag !== LegacyRoot && exitStatus === RootErrored) { + // If something threw an error, try rendering one more time. We'll + // render synchronously to block concurrent data mutations, and we'll + // render at Idle (or lower) so that all pending updates are included. + // If it still fails after the second attempt, we'll give up and commit + // the resulting tree. + expirationTime = expirationTime > Idle ? Idle : expirationTime; + exitStatus = renderRootSync(root, expirationTime); + } + + if (exitStatus === RootFatalErrored) { + const fatalError = workInProgressRootFatalError; + prepareFreshStack(root, expirationTime); + markRootSuspendedAtTime(root, expirationTime); + ensureRootIsScheduled(root); + throw fatalError; + } + + // We now have a consistent tree. Because this is a sync render, we + // will commit it even if something suspended. + const finishedWork: Fiber = (root.current.alternate: any); + root.finishedWork = finishedWork; + root.finishedExpirationTime = expirationTime; + root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork); + commitRoot(root); + + // Before exiting, make sure there's a callback scheduled for the next + // pending level. + ensureRootIsScheduled(root); + + return null; +} + +export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) { + markRootExpiredAtTime(root, expirationTime); + ensureRootIsScheduled(root); + if ((executionContext & (RenderContext | CommitContext)) === NoContext) { + flushSyncCallbackQueue(); + } +} + +export function flushDiscreteUpdates() { + // TODO: Should be able to flush inside batchedUpdates, but not inside `act`. + // However, `act` uses `batchedUpdates`, so there's no way to distinguish + // those two cases. Need to fix this before exposing flushDiscreteUpdates + // as a public API. + if ( + (executionContext & (BatchedContext | RenderContext | CommitContext)) !== + NoContext + ) { + if (__DEV__) { + if ((executionContext & RenderContext) !== NoContext) { + console.error( + 'unstable_flushDiscreteUpdates: Cannot flush updates when React is ' + + 'already rendering.', + ); + } + } + // We're already rendering, so we can't synchronously flush pending work. + // This is probably a nested event dispatch triggered by a lifecycle/effect, + // like `el.focus()`. Exit. + return; + } + flushPendingDiscreteUpdates(); + // If the discrete updates scheduled passive effects, flush them now so that + // they fire before the next serial event. + flushPassiveEffects(); +} + +export function deferredUpdates(fn: () => A): A { + // TODO: Remove in favor of Scheduler.next + return runWithPriority(NormalPriority, fn); +} + +export function syncUpdates( + fn: (A, B, C) => R, + a: A, + b: B, + c: C, +): R { + return runWithPriority(ImmediatePriority, fn.bind(null, a, b, c)); +} + +function flushPendingDiscreteUpdates() { + if (rootsWithPendingDiscreteUpdates !== null) { + // For each root with pending discrete updates, schedule a callback to + // immediately flush them. + const roots = rootsWithPendingDiscreteUpdates; + rootsWithPendingDiscreteUpdates = null; + roots.forEach((expirationTime, root) => { + markRootExpiredAtTime(root, expirationTime); + ensureRootIsScheduled(root); + }); + // Now flush the immediate queue. + flushSyncCallbackQueue(); + } +} + +export function batchedUpdates(fn: A => R, a: A): R { + const prevExecutionContext = executionContext; + executionContext |= BatchedContext; + try { + return fn(a); + } finally { + executionContext = prevExecutionContext; + if (executionContext === NoContext) { + // Flush the immediate callbacks that were scheduled during this batch + flushSyncCallbackQueue(); + } + } +} + +export function batchedEventUpdates(fn: A => R, a: A): R { + const prevExecutionContext = executionContext; + executionContext |= EventContext; + try { + return fn(a); + } finally { + executionContext = prevExecutionContext; + if (executionContext === NoContext) { + // Flush the immediate callbacks that were scheduled during this batch + flushSyncCallbackQueue(); + } + } +} + +export function discreteUpdates( + fn: (A, B, C) => R, + a: A, + b: B, + c: C, + d: D, +): R { + const prevExecutionContext = executionContext; + executionContext |= DiscreteEventContext; + try { + // Should this + return runWithPriority(UserBlockingPriority, fn.bind(null, a, b, c, d)); + } finally { + executionContext = prevExecutionContext; + if (executionContext === NoContext) { + // Flush the immediate callbacks that were scheduled during this batch + flushSyncCallbackQueue(); + } + } +} + +export function unbatchedUpdates(fn: (a: A) => R, a: A): R { + const prevExecutionContext = executionContext; + executionContext &= ~BatchedContext; + executionContext |= LegacyUnbatchedContext; + try { + return fn(a); + } finally { + executionContext = prevExecutionContext; + if (executionContext === NoContext) { + // Flush the immediate callbacks that were scheduled during this batch + flushSyncCallbackQueue(); + } + } +} + +export function flushSync(fn: A => R, a: A): R { + if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { + invariant( + false, + 'flushSync was called from inside a lifecycle method. It cannot be ' + + 'called when React is already rendering.', + ); + } + const prevExecutionContext = executionContext; + executionContext |= BatchedContext; + try { + return runWithPriority(ImmediatePriority, fn.bind(null, a)); + } finally { + executionContext = prevExecutionContext; + // Flush the immediate callbacks that were scheduled during this batch. + // Note that this will happen even if batchedUpdates is higher up + // the stack. + flushSyncCallbackQueue(); + } +} + +export function flushControlled(fn: () => mixed): void { + const prevExecutionContext = executionContext; + executionContext |= BatchedContext; + try { + runWithPriority(ImmediatePriority, fn); + } finally { + executionContext = prevExecutionContext; + if (executionContext === NoContext) { + // Flush the immediate callbacks that were scheduled during this batch + flushSyncCallbackQueue(); + } + } +} + +function prepareFreshStack(root, expirationTime) { + root.finishedWork = null; + root.finishedExpirationTime = NoWork; + + const timeoutHandle = root.timeoutHandle; + if (timeoutHandle !== noTimeout) { + // The root previous suspended and scheduled a timeout to commit a fallback + // state. Now that we have additional work, cancel the timeout. + root.timeoutHandle = noTimeout; + // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above + cancelTimeout(timeoutHandle); + } + + // Check if there's a suspended level at lower priority. + const lastSuspendedTime = root.lastSuspendedTime; + if (lastSuspendedTime !== NoWork && lastSuspendedTime < expirationTime) { + const lastPingedTime = root.lastPingedTime; + // Make sure the suspended level is marked as pinged so that we return back + // to it later, in case the render we're about to start gets aborted. + // Generally we only reach this path via a ping, but we shouldn't assume + // that will always be the case. + // Note: This is defensive coding to prevent a pending commit from + // being dropped without being rescheduled. It shouldn't be necessary. + if (lastPingedTime === NoWork || lastPingedTime > lastSuspendedTime) { + root.lastPingedTime = lastSuspendedTime; + } + } + + if (workInProgress !== null) { + let interruptedWork = workInProgress.return; + while (interruptedWork !== null) { + unwindInterruptedWork(interruptedWork); + interruptedWork = interruptedWork.return; + } + } + workInProgressRoot = root; + workInProgress = createWorkInProgress(root.current, null); + renderExpirationTime = expirationTime; + workInProgressRootExitStatus = RootIncomplete; + workInProgressRootFatalError = null; + workInProgressRootLatestProcessedExpirationTime = Sync; + workInProgressRootLatestSuspenseTimeout = Sync; + workInProgressRootCanSuspendUsingConfig = null; + workInProgressRootNextUnprocessedUpdateTime = NoWork; + workInProgressRootHasPendingPing = false; + + if (enableSchedulerTracing) { + spawnedWorkDuringRender = null; + } + + if (__DEV__) { + ReactStrictModeWarnings.discardPendingWarnings(); + } +} + +function handleError(root, thrownValue) { + do { + try { + // Reset module-level state that was set during the render phase. + resetContextDependencies(); + resetHooksAfterThrow(); + resetCurrentDebugFiberInDEV(); + // TODO: I found and added this missing line while investigating a + // separate issue. Write a regression test using string refs. + ReactCurrentOwner.current = null; + + if (workInProgress === null || workInProgress.return === null) { + // Expected to be working on a non-root fiber. This is a fatal error + // because there's no ancestor that can handle it; the root is + // supposed to capture all errors that weren't caught by an error + // boundary. + workInProgressRootExitStatus = RootFatalErrored; + workInProgressRootFatalError = thrownValue; + // Set `workInProgress` to null. This represents advancing to the next + // sibling, or the parent if there are no siblings. But since the root + // has no siblings nor a parent, we set it to null. Usually this is + // handled by `completeUnitOfWork` or `unwindWork`, but since we're + // interntionally not calling those, we need set it here. + // TODO: Consider calling `unwindWork` to pop the contexts. + workInProgress = null; + return null; + } + + if (enableProfilerTimer && workInProgress.mode & ProfileMode) { + // Record the time spent rendering before an error was thrown. This + // avoids inaccurate Profiler durations in the case of a + // suspended render. + stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true); + } + + throwException( + root, + workInProgress.return, + workInProgress, + thrownValue, + renderExpirationTime, + ); + workInProgress = completeUnitOfWork(workInProgress); + } catch (yetAnotherThrownValue) { + // Something in the return path also threw. + thrownValue = yetAnotherThrownValue; + continue; + } + // Return to the normal work loop. + return; + } while (true); +} + +function pushDispatcher(root) { + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = ContextOnlyDispatcher; + if (prevDispatcher === null) { + // The React isomorphic package does not include a default dispatcher. + // Instead the first renderer will lazily attach one, in order to give + // nicer error messages. + return ContextOnlyDispatcher; + } else { + return prevDispatcher; + } +} + +function popDispatcher(prevDispatcher) { + ReactCurrentDispatcher.current = prevDispatcher; +} + +function pushInteractions(root) { + if (enableSchedulerTracing) { + const prevInteractions: Set | null = __interactionsRef.current; + __interactionsRef.current = root.memoizedInteractions; + return prevInteractions; + } + return null; +} + +function popInteractions(prevInteractions) { + if (enableSchedulerTracing) { + __interactionsRef.current = prevInteractions; + } +} + +export function markCommitTimeOfFallback() { + globalMostRecentFallbackTime = now(); +} + +export function markRenderEventTimeAndConfig( + expirationTime: ExpirationTime, + suspenseConfig: null | SuspenseConfig, +): void { + if ( + expirationTime < workInProgressRootLatestProcessedExpirationTime && + expirationTime > Idle + ) { + workInProgressRootLatestProcessedExpirationTime = expirationTime; + } + if (suspenseConfig !== null) { + if ( + expirationTime < workInProgressRootLatestSuspenseTimeout && + expirationTime > Idle + ) { + workInProgressRootLatestSuspenseTimeout = expirationTime; + // Most of the time we only have one config and getting wrong is not bad. + workInProgressRootCanSuspendUsingConfig = suspenseConfig; + } + } +} + +export function markUnprocessedUpdateTime( + expirationTime: ExpirationTime, +): void { + if (expirationTime > workInProgressRootNextUnprocessedUpdateTime) { + workInProgressRootNextUnprocessedUpdateTime = expirationTime; + } +} + +export function renderDidSuspend(): void { + if (workInProgressRootExitStatus === RootIncomplete) { + workInProgressRootExitStatus = RootSuspended; + } +} + +export function renderDidSuspendDelayIfPossible(): void { + if ( + workInProgressRootExitStatus === RootIncomplete || + workInProgressRootExitStatus === RootSuspended + ) { + workInProgressRootExitStatus = RootSuspendedWithDelay; + } + + // Check if there's a lower priority update somewhere else in the tree. + if ( + workInProgressRootNextUnprocessedUpdateTime !== NoWork && + workInProgressRoot !== null + ) { + // Mark the current render as suspended, and then mark that there's a + // pending update. + // TODO: This should immediately interrupt the current render, instead + // of waiting until the next time we yield. + markRootSuspendedAtTime(workInProgressRoot, renderExpirationTime); + markRootUpdatedAtTime( + workInProgressRoot, + workInProgressRootNextUnprocessedUpdateTime, + ); + } +} + +export function renderDidError() { + if (workInProgressRootExitStatus !== RootCompleted) { + workInProgressRootExitStatus = RootErrored; + } +} + +// Called during render to determine if anything has suspended. +// Returns false if we're not sure. +export function renderHasNotSuspendedYet(): boolean { + // If something errored or completed, we can't really be sure, + // so those are false. + return workInProgressRootExitStatus === RootIncomplete; +} + +function inferTimeFromExpirationTime(expirationTime: ExpirationTime): number { + // We don't know exactly when the update was scheduled, but we can infer an + // approximate start time from the expiration time. + const earliestExpirationTimeMs = expirationTimeToMs(expirationTime); + return earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION; +} + +function inferTimeFromExpirationTimeWithSuspenseConfig( + expirationTime: ExpirationTime, + suspenseConfig: SuspenseConfig, +): number { + // We don't know exactly when the update was scheduled, but we can infer an + // approximate start time from the expiration time by subtracting the timeout + // that was added to the event time. + const earliestExpirationTimeMs = expirationTimeToMs(expirationTime); + return ( + earliestExpirationTimeMs - + (suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION) + ); +} + +function renderRootSync(root, expirationTime) { + const prevExecutionContext = executionContext; + executionContext |= RenderContext; + const prevDispatcher = pushDispatcher(root); + + // If the root or expiration time have changed, throw out the existing stack + // and prepare a fresh one. Otherwise we'll continue where we left off. + if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) { + prepareFreshStack(root, expirationTime); + startWorkOnPendingInteractions(root, expirationTime); + } + + const prevInteractions = pushInteractions(root); + do { + try { + workLoopSync(); + break; + } catch (thrownValue) { + handleError(root, thrownValue); + } + } while (true); + resetContextDependencies(); + if (enableSchedulerTracing) { + popInteractions(((prevInteractions: any): Set)); + } + + executionContext = prevExecutionContext; + popDispatcher(prevDispatcher); + + if (workInProgress !== null) { + // This is a sync render, so we should have finished the whole tree. + invariant( + false, + 'Cannot commit an incomplete root. This error is likely caused by a ' + + 'bug in React. Please file an issue.', + ); + } + + // Set this to null to indicate there's no in-progress render. + workInProgressRoot = null; + + return workInProgressRootExitStatus; +} + +// The work loop is an extremely hot path. Tell Closure not to inline it. +/** @noinline */ +function workLoopSync() { + // Already timed out, so perform work without checking if we need to yield. + while (workInProgress !== null) { + workInProgress = performUnitOfWork(workInProgress); + } +} + +function renderRootConcurrent(root, expirationTime) { + const prevExecutionContext = executionContext; + executionContext |= RenderContext; + const prevDispatcher = pushDispatcher(root); + + // If the root or expiration time have changed, throw out the existing stack + // and prepare a fresh one. Otherwise we'll continue where we left off. + if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) { + prepareFreshStack(root, expirationTime); + startWorkOnPendingInteractions(root, expirationTime); + } + + const prevInteractions = pushInteractions(root); + do { + try { + workLoopConcurrent(); + break; + } catch (thrownValue) { + handleError(root, thrownValue); + } + } while (true); + resetContextDependencies(); + if (enableSchedulerTracing) { + popInteractions(((prevInteractions: any): Set)); + } + + popDispatcher(prevDispatcher); + executionContext = prevExecutionContext; + + // Check if the tree has completed. + if (workInProgress !== null) { + // Still work remaining. + return RootIncomplete; + } else { + // Completed the tree. + // Set this to null to indicate there's no in-progress render. + workInProgressRoot = null; + + // Return the final exit status. + return workInProgressRootExitStatus; + } +} + +/** @noinline */ +function workLoopConcurrent() { + // Perform work until Scheduler asks us to yield + while (workInProgress !== null && !shouldYield()) { + workInProgress = performUnitOfWork(workInProgress); + } +} + +function performUnitOfWork(unitOfWork: Fiber): Fiber | null { + // The current, flushed, state of this fiber is the alternate. Ideally + // nothing should rely on this, but relying on it here means that we don't + // need an additional field on the work in progress. + const current = unitOfWork.alternate; + setCurrentDebugFiberInDEV(unitOfWork); + + let next; + if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) { + startProfilerTimer(unitOfWork); + next = beginWork(current, unitOfWork, renderExpirationTime); + stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); + } else { + next = beginWork(current, unitOfWork, renderExpirationTime); + } + + resetCurrentDebugFiberInDEV(); + unitOfWork.memoizedProps = unitOfWork.pendingProps; + if (next === null) { + // If this doesn't spawn new work, complete the current work. + next = completeUnitOfWork(unitOfWork); + } + + ReactCurrentOwner.current = null; + return next; +} + +function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { + // Attempt to complete the current unit of work, then move to the next + // sibling. If there are no more siblings, return to the parent fiber. + workInProgress = unitOfWork; + do { + // The current, flushed, state of this fiber is the alternate. Ideally + // nothing should rely on this, but relying on it here means that we don't + // need an additional field on the work in progress. + const current = workInProgress.alternate; + const returnFiber = workInProgress.return; + + // Check if the work completed or if something threw. + if ((workInProgress.effectTag & Incomplete) === NoEffect) { + setCurrentDebugFiberInDEV(workInProgress); + let next; + if ( + !enableProfilerTimer || + (workInProgress.mode & ProfileMode) === NoMode + ) { + next = completeWork(current, workInProgress, renderExpirationTime); + } else { + startProfilerTimer(workInProgress); + next = completeWork(current, workInProgress, renderExpirationTime); + // Update render duration assuming we didn't error. + stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false); + } + resetCurrentDebugFiberInDEV(); + resetChildExpirationTime(workInProgress); + + if (next !== null) { + // Completing this fiber spawned new work. Work on that next. + return next; + } + + if ( + returnFiber !== null && + // Do not append effects to parents if a sibling failed to complete + (returnFiber.effectTag & Incomplete) === NoEffect + ) { + // Append all the effects of the subtree and this fiber onto the effect + // list of the parent. The completion order of the children affects the + // side-effect order. + if (returnFiber.firstEffect === null) { + returnFiber.firstEffect = workInProgress.firstEffect; + } + if (workInProgress.lastEffect !== null) { + if (returnFiber.lastEffect !== null) { + returnFiber.lastEffect.nextEffect = workInProgress.firstEffect; + } + returnFiber.lastEffect = workInProgress.lastEffect; + } + + // If this fiber had side-effects, we append it AFTER the children's + // side-effects. We can perform certain side-effects earlier if needed, + // by doing multiple passes over the effect list. We don't want to + // schedule our own side-effect on our own list because if end up + // reusing children we'll schedule this effect onto itself since we're + // at the end. + const effectTag = workInProgress.effectTag; + + // Skip both NoWork and PerformedWork tags when creating the effect + // list. PerformedWork effect is read by React DevTools but shouldn't be + // committed. + if (effectTag > PerformedWork) { + if (returnFiber.lastEffect !== null) { + returnFiber.lastEffect.nextEffect = workInProgress; + } else { + returnFiber.firstEffect = workInProgress; + } + returnFiber.lastEffect = workInProgress; + } + } + } else { + // This fiber did not complete because something threw. Pop values off + // the stack without entering the complete phase. If this is a boundary, + // capture values if possible. + const next = unwindWork(workInProgress, renderExpirationTime); + + // Because this fiber did not complete, don't reset its expiration time. + + if ( + enableProfilerTimer && + (workInProgress.mode & ProfileMode) !== NoMode + ) { + // Record the render duration for the fiber that errored. + stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false); + + // Include the time spent working on failed children before continuing. + let actualDuration = workInProgress.actualDuration; + let child = workInProgress.child; + while (child !== null) { + actualDuration += child.actualDuration; + child = child.sibling; + } + workInProgress.actualDuration = actualDuration; + } + + if (next !== null) { + // If completing this work spawned new work, do that next. We'll come + // back here again. + // Since we're restarting, remove anything that is not a host effect + // from the effect tag. + next.effectTag &= HostEffectMask; + return next; + } + + if (returnFiber !== null) { + // Mark the parent fiber as incomplete and clear its effect list. + returnFiber.firstEffect = returnFiber.lastEffect = null; + returnFiber.effectTag |= Incomplete; + } + } + + const siblingFiber = workInProgress.sibling; + if (siblingFiber !== null) { + // If there is more work to do in this returnFiber, do that next. + return siblingFiber; + } + // Otherwise, return to the parent + workInProgress = returnFiber; + } while (workInProgress !== null); + + // We've reached the root. + if (workInProgressRootExitStatus === RootIncomplete) { + workInProgressRootExitStatus = RootCompleted; + } + return null; +} + +function getRemainingExpirationTime(fiber: Fiber) { + const updateExpirationTime = fiber.expirationTime; + const childExpirationTime = fiber.childExpirationTime; + return updateExpirationTime > childExpirationTime + ? updateExpirationTime + : childExpirationTime; +} + +function resetChildExpirationTime(completedWork: Fiber) { + if ( + renderExpirationTime !== Never && + completedWork.childExpirationTime === Never + ) { + // The children of this component are hidden. Don't bubble their + // expiration times. + return; + } + + let newChildExpirationTime = NoWork; + + // Bubble up the earliest expiration time. + if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) { + // In profiling mode, resetChildExpirationTime is also used to reset + // profiler durations. + let actualDuration = completedWork.actualDuration; + let treeBaseDuration = completedWork.selfBaseDuration; + + // When a fiber is cloned, its actualDuration is reset to 0. This value will + // only be updated if work is done on the fiber (i.e. it doesn't bailout). + // When work is done, it should bubble to the parent's actualDuration. If + // the fiber has not been cloned though, (meaning no work was done), then + // this value will reflect the amount of time spent working on a previous + // render. In that case it should not bubble. We determine whether it was + // cloned by comparing the child pointer. + const shouldBubbleActualDurations = + completedWork.alternate === null || + completedWork.child !== completedWork.alternate.child; + + let child = completedWork.child; + while (child !== null) { + const childUpdateExpirationTime = child.expirationTime; + const childChildExpirationTime = child.childExpirationTime; + if (childUpdateExpirationTime > newChildExpirationTime) { + newChildExpirationTime = childUpdateExpirationTime; + } + if (childChildExpirationTime > newChildExpirationTime) { + newChildExpirationTime = childChildExpirationTime; + } + if (shouldBubbleActualDurations) { + actualDuration += child.actualDuration; + } + treeBaseDuration += child.treeBaseDuration; + child = child.sibling; + } + completedWork.actualDuration = actualDuration; + completedWork.treeBaseDuration = treeBaseDuration; + } else { + let child = completedWork.child; + while (child !== null) { + const childUpdateExpirationTime = child.expirationTime; + const childChildExpirationTime = child.childExpirationTime; + if (childUpdateExpirationTime > newChildExpirationTime) { + newChildExpirationTime = childUpdateExpirationTime; + } + if (childChildExpirationTime > newChildExpirationTime) { + newChildExpirationTime = childChildExpirationTime; + } + child = child.sibling; + } + } + + completedWork.childExpirationTime = newChildExpirationTime; +} + +function commitRoot(root) { + const renderPriorityLevel = getCurrentPriorityLevel(); + runWithPriority( + ImmediatePriority, + commitRootImpl.bind(null, root, renderPriorityLevel), + ); + return null; +} + +function commitRootImpl(root, renderPriorityLevel) { + do { + // `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which + // means `flushPassiveEffects` will sometimes result in additional + // passive effects. So we need to keep flushing in a loop until there are + // no more pending effects. + // TODO: Might be better if `flushPassiveEffects` did not automatically + // flush synchronous work at the end, to avoid factoring hazards like this. + flushPassiveEffects(); + } while (rootWithPendingPassiveEffects !== null); + flushRenderPhaseStrictModeWarningsInDEV(); + + invariant( + (executionContext & (RenderContext | CommitContext)) === NoContext, + 'Should not already be working.', + ); + + const finishedWork = root.finishedWork; + const expirationTime = root.finishedExpirationTime; + if (finishedWork === null) { + return null; + } + root.finishedWork = null; + root.finishedExpirationTime = NoWork; + + invariant( + finishedWork !== root.current, + 'Cannot commit the same tree as before. This error is likely caused by ' + + 'a bug in React. Please file an issue.', + ); + + // commitRoot never returns a continuation; it always finishes synchronously. + // So we can clear these now to allow a new callback to be scheduled. + root.callbackNode = null; + root.callbackExpirationTime = NoWork; + root.callbackPriority = NoPriority; + + // Update the first and last pending times on this root. The new first + // pending time is whatever is left on the root fiber. + const remainingExpirationTimeBeforeCommit = getRemainingExpirationTime( + finishedWork, + ); + markRootFinishedAtTime( + root, + expirationTime, + remainingExpirationTimeBeforeCommit, + ); + + // Clear already finished discrete updates in case that a later call of + // `flushDiscreteUpdates` starts a useless render pass which may cancels + // a scheduled timeout. + if (rootsWithPendingDiscreteUpdates !== null) { + const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root); + if ( + lastDiscreteTime !== undefined && + remainingExpirationTimeBeforeCommit < lastDiscreteTime + ) { + rootsWithPendingDiscreteUpdates.delete(root); + } + } + + if (root === workInProgressRoot) { + // We can reset these now that they are finished. + workInProgressRoot = null; + workInProgress = null; + renderExpirationTime = NoWork; + } else { + // This indicates that the last root we worked on is not the same one that + // we're committing now. This most commonly happens when a suspended root + // times out. + } + + // Get the list of effects. + let firstEffect; + if (finishedWork.effectTag > PerformedWork) { + // A fiber's effect list consists only of its children, not itself. So if + // the root has an effect, we need to add it to the end of the list. The + // resulting list is the set that would belong to the root's parent, if it + // had one; that is, all the effects in the tree including the root. + if (finishedWork.lastEffect !== null) { + finishedWork.lastEffect.nextEffect = finishedWork; + firstEffect = finishedWork.firstEffect; + } else { + firstEffect = finishedWork; + } + } else { + // There is no effect on the root. + firstEffect = finishedWork.firstEffect; + } + + if (firstEffect !== null) { + const prevExecutionContext = executionContext; + executionContext |= CommitContext; + const prevInteractions = pushInteractions(root); + + // Reset this to null before calling lifecycles + ReactCurrentOwner.current = null; + + // The commit phase is broken into several sub-phases. We do a separate pass + // of the effect list for each phase: all mutation effects come before all + // layout effects, and so on. + + // The first phase a "before mutation" phase. We use this phase to read the + // state of the host tree right before we mutate it. This is where + // getSnapshotBeforeUpdate is called. + prepareForCommit(root.containerInfo); + nextEffect = firstEffect; + do { + if (__DEV__) { + invokeGuardedCallback(null, commitBeforeMutationEffects, null); + if (hasCaughtError()) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + const error = clearCaughtError(); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } else { + try { + commitBeforeMutationEffects(); + } catch (error) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } + } while (nextEffect !== null); + + if (enableProfilerTimer) { + // Mark the current commit time to be shared by all Profilers in this + // batch. This enables them to be grouped later. + recordCommitTime(); + } + + // The next phase is the mutation phase, where we mutate the host tree. + nextEffect = firstEffect; + do { + if (__DEV__) { + invokeGuardedCallback( + null, + commitMutationEffects, + null, + root, + renderPriorityLevel, + ); + if (hasCaughtError()) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + const error = clearCaughtError(); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } else { + try { + commitMutationEffects(root, renderPriorityLevel); + } catch (error) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } + } while (nextEffect !== null); + resetAfterCommit(root.containerInfo); + + // The work-in-progress tree is now the current tree. This must come after + // the mutation phase, so that the previous tree is still current during + // componentWillUnmount, but before the layout phase, so that the finished + // work is current during componentDidMount/Update. + root.current = finishedWork; + + // The next phase is the layout phase, where we call effects that read + // the host tree after it's been mutated. The idiomatic use case for this is + // layout, but class component lifecycles also fire here for legacy reasons. + nextEffect = firstEffect; + do { + if (__DEV__) { + invokeGuardedCallback( + null, + commitLayoutEffects, + null, + root, + expirationTime, + ); + if (hasCaughtError()) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + const error = clearCaughtError(); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } else { + try { + commitLayoutEffects(root, expirationTime); + } catch (error) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } + } while (nextEffect !== null); + + nextEffect = null; + + // Tell Scheduler to yield at the end of the frame, so the browser has an + // opportunity to paint. + requestPaint(); + + if (enableSchedulerTracing) { + popInteractions(((prevInteractions: any): Set)); + } + executionContext = prevExecutionContext; + } else { + // No effects. + root.current = finishedWork; + // Measure these anyway so the flamegraph explicitly shows that there were + // no effects. + // TODO: Maybe there's a better way to report this. + if (enableProfilerTimer) { + recordCommitTime(); + } + } + + const rootDidHavePassiveEffects = rootDoesHavePassiveEffects; + + if (rootDoesHavePassiveEffects) { + // This commit has passive effects. Stash a reference to them. But don't + // schedule a callback until after flushing layout work. + rootDoesHavePassiveEffects = false; + rootWithPendingPassiveEffects = root; + pendingPassiveEffectsExpirationTime = expirationTime; + pendingPassiveEffectsRenderPriority = renderPriorityLevel; + } else { + // We are done with the effect chain at this point so let's clear the + // nextEffect pointers to assist with GC. If we have passive effects, we'll + // clear this in flushPassiveEffects. + nextEffect = firstEffect; + while (nextEffect !== null) { + const nextNextEffect = nextEffect.nextEffect; + nextEffect.nextEffect = null; + nextEffect = nextNextEffect; + } + } + + // Check if there's remaining work on this root + const remainingExpirationTime = root.firstPendingTime; + if (remainingExpirationTime !== NoWork) { + if (enableSchedulerTracing) { + if (spawnedWorkDuringRender !== null) { + const expirationTimes = spawnedWorkDuringRender; + spawnedWorkDuringRender = null; + for (let i = 0; i < expirationTimes.length; i++) { + scheduleInteractions( + root, + expirationTimes[i], + root.memoizedInteractions, + ); + } + } + schedulePendingInteractions(root, remainingExpirationTime); + } + } else { + // If there's no remaining work, we can clear the set of already failed + // error boundaries. + legacyErrorBoundariesThatAlreadyFailed = null; + } + + if (enableSchedulerTracing) { + if (!rootDidHavePassiveEffects) { + // If there are no passive effects, then we can complete the pending interactions. + // Otherwise, we'll wait until after the passive effects are flushed. + // Wait to do this until after remaining work has been scheduled, + // so that we don't prematurely signal complete for interactions when there's e.g. hidden work. + finishPendingInteractions(root, expirationTime); + } + } + + if (remainingExpirationTime === Sync) { + // Count the number of times the root synchronously re-renders without + // finishing. If there are too many, it indicates an infinite update loop. + if (root === rootWithNestedUpdates) { + nestedUpdateCount++; + } else { + nestedUpdateCount = 0; + rootWithNestedUpdates = root; + } + } else { + nestedUpdateCount = 0; + } + + onCommitRoot(finishedWork.stateNode, expirationTime); + + // Always call this before exiting `commitRoot`, to ensure that any + // additional work on this root is scheduled. + ensureRootIsScheduled(root); + + if (hasUncaughtError) { + hasUncaughtError = false; + const error = firstUncaughtError; + firstUncaughtError = null; + throw error; + } + + if ((executionContext & LegacyUnbatchedContext) !== NoContext) { + // This is a legacy edge case. We just committed the initial mount of + // a ReactDOM.render-ed root inside of batchedUpdates. The commit fired + // synchronously, but layout updates should be deferred until the end + // of the batch. + return null; + } + + // If layout work was scheduled, flush it now. + flushSyncCallbackQueue(); + return null; +} + +function commitBeforeMutationEffects() { + while (nextEffect !== null) { + const effectTag = nextEffect.effectTag; + if ((effectTag & Snapshot) !== NoEffect) { + setCurrentDebugFiberInDEV(nextEffect); + + const current = nextEffect.alternate; + commitBeforeMutationEffectOnFiber(current, nextEffect); + + resetCurrentDebugFiberInDEV(); + } + if ((effectTag & Passive) !== NoEffect) { + // If there are passive effects, schedule a callback to flush at + // the earliest opportunity. + if (!rootDoesHavePassiveEffects) { + rootDoesHavePassiveEffects = true; + scheduleCallback(NormalPriority, () => { + flushPassiveEffects(); + return null; + }); + } + } + nextEffect = nextEffect.nextEffect; + } +} + +function commitMutationEffects(root: FiberRoot, renderPriorityLevel) { + // TODO: Should probably move the bulk of this function to commitWork. + while (nextEffect !== null) { + setCurrentDebugFiberInDEV(nextEffect); + + const effectTag = nextEffect.effectTag; + + if (effectTag & ContentReset) { + commitResetTextContent(nextEffect); + } + + if (effectTag & Ref) { + const current = nextEffect.alternate; + if (current !== null) { + commitDetachRef(current); + } + } + + // The following switch statement is only concerned about placement, + // updates, and deletions. To avoid needing to add a case for every possible + // bitmap value, we remove the secondary effects from the effect tag and + // switch on that value. + const primaryEffectTag = + effectTag & (Placement | Update | Deletion | Hydrating); + switch (primaryEffectTag) { + case Placement: { + commitPlacement(nextEffect); + // Clear the "placement" from effect tag so that we know that this is + // inserted, before any life-cycles like componentDidMount gets called. + // TODO: findDOMNode doesn't rely on this any more but isMounted does + // and isMounted is deprecated anyway so we should be able to kill this. + nextEffect.effectTag &= ~Placement; + break; + } + case PlacementAndUpdate: { + // Placement + commitPlacement(nextEffect); + // Clear the "placement" from effect tag so that we know that this is + // inserted, before any life-cycles like componentDidMount gets called. + nextEffect.effectTag &= ~Placement; + + // Update + const current = nextEffect.alternate; + commitWork(current, nextEffect); + break; + } + case Hydrating: { + nextEffect.effectTag &= ~Hydrating; + break; + } + case HydratingAndUpdate: { + nextEffect.effectTag &= ~Hydrating; + + // Update + const current = nextEffect.alternate; + commitWork(current, nextEffect); + break; + } + case Update: { + const current = nextEffect.alternate; + commitWork(current, nextEffect); + break; + } + case Deletion: { + commitDeletion(root, nextEffect, renderPriorityLevel); + break; + } + } + + resetCurrentDebugFiberInDEV(); + nextEffect = nextEffect.nextEffect; + } +} + +function commitLayoutEffects( + root: FiberRoot, + committedExpirationTime: ExpirationTime, +) { + // TODO: Should probably move the bulk of this function to commitWork. + while (nextEffect !== null) { + setCurrentDebugFiberInDEV(nextEffect); + + const effectTag = nextEffect.effectTag; + + if (effectTag & (Update | Callback)) { + const current = nextEffect.alternate; + commitLayoutEffectOnFiber( + root, + current, + nextEffect, + committedExpirationTime, + ); + } + + if (effectTag & Ref) { + commitAttachRef(nextEffect); + } + + resetCurrentDebugFiberInDEV(); + nextEffect = nextEffect.nextEffect; + } +} + +export function flushPassiveEffects() { + if (pendingPassiveEffectsRenderPriority !== NoPriority) { + const priorityLevel = + pendingPassiveEffectsRenderPriority > NormalPriority + ? NormalPriority + : pendingPassiveEffectsRenderPriority; + pendingPassiveEffectsRenderPriority = NoPriority; + return runWithPriority(priorityLevel, flushPassiveEffectsImpl); + } +} + +export function enqueuePendingPassiveProfilerEffect(fiber: Fiber): void { + if (enableProfilerTimer && enableProfilerCommitHooks) { + pendingPassiveProfilerEffects.push(fiber); + if (!rootDoesHavePassiveEffects) { + rootDoesHavePassiveEffects = true; + scheduleCallback(NormalPriority, () => { + flushPassiveEffects(); + return null; + }); + } + } +} + +export function enqueuePendingPassiveHookEffectMount( + fiber: Fiber, + effect: HookEffect, +): void { + if (runAllPassiveEffectDestroysBeforeCreates) { + pendingPassiveHookEffectsMount.push(effect, fiber); + if (!rootDoesHavePassiveEffects) { + rootDoesHavePassiveEffects = true; + scheduleCallback(NormalPriority, () => { + flushPassiveEffects(); + return null; + }); + } + } +} + +export function enqueuePendingPassiveHookEffectUnmount( + fiber: Fiber, + effect: HookEffect, +): void { + if (runAllPassiveEffectDestroysBeforeCreates) { + pendingPassiveHookEffectsUnmount.push(effect, fiber); + if (!rootDoesHavePassiveEffects) { + rootDoesHavePassiveEffects = true; + scheduleCallback(NormalPriority, () => { + flushPassiveEffects(); + return null; + }); + } + } +} + +function invokePassiveEffectCreate(effect: HookEffect): void { + const create = effect.create; + effect.destroy = create(); +} + +function flushPassiveEffectsImpl() { + if (rootWithPendingPassiveEffects === null) { + return false; + } + + const root = rootWithPendingPassiveEffects; + const expirationTime = pendingPassiveEffectsExpirationTime; + rootWithPendingPassiveEffects = null; + pendingPassiveEffectsExpirationTime = NoWork; + + invariant( + (executionContext & (RenderContext | CommitContext)) === NoContext, + 'Cannot flush passive effects while already rendering.', + ); + + if (__DEV__) { + isFlushingPassiveEffects = true; + } + + const prevExecutionContext = executionContext; + executionContext |= CommitContext; + const prevInteractions = pushInteractions(root); + + if (runAllPassiveEffectDestroysBeforeCreates) { + // It's important that ALL pending passive effect destroy functions are called + // before ANY passive effect create functions are called. + // Otherwise effects in sibling components might interfere with each other. + // e.g. a destroy function in one component may unintentionally override a ref + // value set by a create function in another component. + // Layout effects have the same constraint. + + // First pass: Destroy stale passive effects. + const unmountEffects = pendingPassiveHookEffectsUnmount; + pendingPassiveHookEffectsUnmount = []; + for (let i = 0; i < unmountEffects.length; i += 2) { + const effect = ((unmountEffects[i]: any): HookEffect); + const fiber = ((unmountEffects[i + 1]: any): Fiber); + const destroy = effect.destroy; + effect.destroy = undefined; + if (typeof destroy === 'function') { + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + fiber.mode & ProfileMode + ) { + startPassiveEffectTimer(); + invokeGuardedCallback(null, destroy, null); + recordPassiveEffectDuration(fiber); + } else { + invokeGuardedCallback(null, destroy, null); + } + if (hasCaughtError()) { + invariant(fiber !== null, 'Should be working on an effect.'); + const error = clearCaughtError(); + captureCommitPhaseError(fiber, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + fiber.mode & ProfileMode + ) { + try { + startPassiveEffectTimer(); + destroy(); + } finally { + recordPassiveEffectDuration(fiber); + } + } else { + destroy(); + } + } catch (error) { + invariant(fiber !== null, 'Should be working on an effect.'); + captureCommitPhaseError(fiber, error); + } + } + } + } + // Second pass: Create new passive effects. + const mountEffects = pendingPassiveHookEffectsMount; + pendingPassiveHookEffectsMount = []; + for (let i = 0; i < mountEffects.length; i += 2) { + const effect = ((mountEffects[i]: any): HookEffect); + const fiber = ((mountEffects[i + 1]: any): Fiber); + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + fiber.mode & ProfileMode + ) { + startPassiveEffectTimer(); + invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect); + recordPassiveEffectDuration(fiber); + } else { + invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect); + } + if (hasCaughtError()) { + invariant(fiber !== null, 'Should be working on an effect.'); + const error = clearCaughtError(); + captureCommitPhaseError(fiber, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + const create = effect.create; + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + fiber.mode & ProfileMode + ) { + try { + startPassiveEffectTimer(); + effect.destroy = create(); + } finally { + recordPassiveEffectDuration(fiber); + } + } else { + effect.destroy = create(); + } + } catch (error) { + invariant(fiber !== null, 'Should be working on an effect.'); + captureCommitPhaseError(fiber, error); + } + } + } + } else { + // Note: This currently assumes there are no passive effects on the root fiber + // because the root is not part of its own effect list. + // This could change in the future. + let effect = root.current.firstEffect; + while (effect !== null) { + if (__DEV__) { + setCurrentDebugFiberInDEV(effect); + invokeGuardedCallback(null, commitPassiveHookEffects, null, effect); + if (hasCaughtError()) { + invariant(effect !== null, 'Should be working on an effect.'); + const error = clearCaughtError(); + captureCommitPhaseError(effect, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitPassiveHookEffects(effect); + } catch (error) { + invariant(effect !== null, 'Should be working on an effect.'); + captureCommitPhaseError(effect, error); + } + } + + const nextNextEffect = effect.nextEffect; + // Remove nextEffect pointer to assist GC + effect.nextEffect = null; + effect = nextNextEffect; + } + } + + if (enableProfilerTimer && enableProfilerCommitHooks) { + const profilerEffects = pendingPassiveProfilerEffects; + pendingPassiveProfilerEffects = []; + for (let i = 0; i < profilerEffects.length; i++) { + const fiber = ((profilerEffects[i]: any): Fiber); + commitPassiveEffectDurations(root, fiber); + } + } + + if (enableSchedulerTracing) { + popInteractions(((prevInteractions: any): Set)); + finishPendingInteractions(root, expirationTime); + } + + if (__DEV__) { + isFlushingPassiveEffects = false; + } + + executionContext = prevExecutionContext; + + flushSyncCallbackQueue(); + + // If additional passive effects were scheduled, increment a counter. If this + // exceeds the limit, we'll fire a warning. + nestedPassiveUpdateCount = + rootWithPendingPassiveEffects === null ? 0 : nestedPassiveUpdateCount + 1; + + return true; +} + +export function isAlreadyFailedLegacyErrorBoundary(instance: mixed): boolean { + return ( + legacyErrorBoundariesThatAlreadyFailed !== null && + legacyErrorBoundariesThatAlreadyFailed.has(instance) + ); +} + +export function markLegacyErrorBoundaryAsFailed(instance: mixed) { + if (legacyErrorBoundariesThatAlreadyFailed === null) { + legacyErrorBoundariesThatAlreadyFailed = new Set([instance]); + } else { + legacyErrorBoundariesThatAlreadyFailed.add(instance); + } +} + +function prepareToThrowUncaughtError(error: mixed) { + if (!hasUncaughtError) { + hasUncaughtError = true; + firstUncaughtError = error; + } +} +export const onUncaughtError = prepareToThrowUncaughtError; + +function captureCommitPhaseErrorOnRoot( + rootFiber: Fiber, + sourceFiber: Fiber, + error: mixed, +) { + const errorInfo = createCapturedValue(error, sourceFiber); + const update = createRootErrorUpdate(rootFiber, errorInfo, Sync); + enqueueUpdate(rootFiber, update); + const root = markUpdateTimeFromFiberToRoot(rootFiber, Sync); + if (root !== null) { + ensureRootIsScheduled(root); + schedulePendingInteractions(root, Sync); + } +} + +export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) { + if (sourceFiber.tag === HostRoot) { + // Error was thrown at the root. There is no parent, so the root + // itself should capture it. + captureCommitPhaseErrorOnRoot(sourceFiber, sourceFiber, error); + return; + } + + let fiber = sourceFiber.return; + while (fiber !== null) { + if (fiber.tag === HostRoot) { + captureCommitPhaseErrorOnRoot(fiber, sourceFiber, error); + return; + } else if (fiber.tag === ClassComponent) { + const ctor = fiber.type; + const instance = fiber.stateNode; + if ( + typeof ctor.getDerivedStateFromError === 'function' || + (typeof instance.componentDidCatch === 'function' && + !isAlreadyFailedLegacyErrorBoundary(instance)) + ) { + const errorInfo = createCapturedValue(error, sourceFiber); + const update = createClassErrorUpdate( + fiber, + errorInfo, + // TODO: This is always sync + Sync, + ); + enqueueUpdate(fiber, update); + const root = markUpdateTimeFromFiberToRoot(fiber, Sync); + if (root !== null) { + ensureRootIsScheduled(root); + schedulePendingInteractions(root, Sync); + } + return; + } + } + fiber = fiber.return; + } +} + +export function pingSuspendedRoot( + root: FiberRoot, + wakeable: Wakeable, + suspendedTime: ExpirationTime, +) { + const pingCache = root.pingCache; + if (pingCache !== null) { + // The wakeable resolved, so we no longer need to memoize, because it will + // never be thrown again. + pingCache.delete(wakeable); + } + + if (workInProgressRoot === root && renderExpirationTime === suspendedTime) { + // Received a ping at the same priority level at which we're currently + // rendering. We might want to restart this render. This should mirror + // the logic of whether or not a root suspends once it completes. + + // TODO: If we're rendering sync either due to Sync, Batched or expired, + // we should probably never restart. + + // If we're suspended with delay, we'll always suspend so we can always + // restart. If we're suspended without any updates, it might be a retry. + // If it's early in the retry we can restart. We can't know for sure + // whether we'll eventually process an update during this render pass, + // but it's somewhat unlikely that we get to a ping before that, since + // getting to the root most update is usually very fast. + if ( + workInProgressRootExitStatus === RootSuspendedWithDelay || + (workInProgressRootExitStatus === RootSuspended && + workInProgressRootLatestProcessedExpirationTime === Sync && + now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS) + ) { + // Restart from the root. Don't need to schedule a ping because + // we're already working on this tree. + prepareFreshStack(root, renderExpirationTime); + } else { + // Even though we can't restart right now, we might get an + // opportunity later. So we mark this render as having a ping. + workInProgressRootHasPendingPing = true; + } + return; + } + + if (!isRootSuspendedAtTime(root, suspendedTime)) { + // The root is no longer suspended at this time. + return; + } + + const lastPingedTime = root.lastPingedTime; + if (lastPingedTime !== NoWork && lastPingedTime < suspendedTime) { + // There's already a lower priority ping scheduled. + return; + } + + // Mark the time at which this ping was scheduled. + root.lastPingedTime = suspendedTime; + + ensureRootIsScheduled(root); + schedulePendingInteractions(root, suspendedTime); +} + +function retryTimedOutBoundary( + boundaryFiber: Fiber, + retryTime: ExpirationTime, +) { + // The boundary fiber (a Suspense component or SuspenseList component) + // previously was rendered in its fallback state. One of the promises that + // suspended it has resolved, which means at least part of the tree was + // likely unblocked. Try rendering again, at a new expiration time. + if (retryTime === NoWork) { + const suspenseConfig = null; // Retries don't carry over the already committed update. + const currentTime = requestCurrentTimeForUpdate(); + retryTime = computeExpirationForFiber( + currentTime, + boundaryFiber, + suspenseConfig, + ); + } + // TODO: Special case idle priority? + const root = markUpdateTimeFromFiberToRoot(boundaryFiber, retryTime); + if (root !== null) { + ensureRootIsScheduled(root); + schedulePendingInteractions(root, retryTime); + } +} + +export function retryDehydratedSuspenseBoundary(boundaryFiber: Fiber) { + const suspenseState: null | SuspenseState = boundaryFiber.memoizedState; + let retryTime = NoWork; + if (suspenseState !== null) { + retryTime = suspenseState.retryTime; + } + retryTimedOutBoundary(boundaryFiber, retryTime); +} + +export function resolveRetryWakeable(boundaryFiber: Fiber, wakeable: Wakeable) { + let retryTime = NoWork; // Default + let retryCache: WeakSet | Set | null; + if (enableSuspenseServerRenderer) { + switch (boundaryFiber.tag) { + case SuspenseComponent: + retryCache = boundaryFiber.stateNode; + const suspenseState: null | SuspenseState = boundaryFiber.memoizedState; + if (suspenseState !== null) { + retryTime = suspenseState.retryTime; + } + break; + case SuspenseListComponent: + retryCache = boundaryFiber.stateNode; + break; + default: + invariant( + false, + 'Pinged unknown suspense boundary type. ' + + 'This is probably a bug in React.', + ); + } + } else { + retryCache = boundaryFiber.stateNode; + } + + if (retryCache !== null) { + // The wakeable resolved, so we no longer need to memoize, because it will + // never be thrown again. + retryCache.delete(wakeable); + } + + retryTimedOutBoundary(boundaryFiber, retryTime); +} + +// Computes the next Just Noticeable Difference (JND) boundary. +// The theory is that a person can't tell the difference between small differences in time. +// Therefore, if we wait a bit longer than necessary that won't translate to a noticeable +// difference in the experience. However, waiting for longer might mean that we can avoid +// showing an intermediate loading state. The longer we have already waited, the harder it +// is to tell small differences in time. Therefore, the longer we've already waited, +// the longer we can wait additionally. At some point we have to give up though. +// We pick a train model where the next boundary commits at a consistent schedule. +// These particular numbers are vague estimates. We expect to adjust them based on research. +function jnd(timeElapsed: number) { + return timeElapsed < 120 + ? 120 + : timeElapsed < 480 + ? 480 + : timeElapsed < 1080 + ? 1080 + : timeElapsed < 1920 + ? 1920 + : timeElapsed < 3000 + ? 3000 + : timeElapsed < 4320 + ? 4320 + : ceil(timeElapsed / 1960) * 1960; +} + +function computeMsUntilSuspenseLoadingDelay( + mostRecentEventTime: ExpirationTime, + committedExpirationTime: ExpirationTime, + suspenseConfig: SuspenseConfig, +) { + const busyMinDurationMs = (suspenseConfig.busyMinDurationMs: any) | 0; + if (busyMinDurationMs <= 0) { + return 0; + } + const busyDelayMs = (suspenseConfig.busyDelayMs: any) | 0; + + // Compute the time until this render pass would expire. + const currentTimeMs: number = now(); + const eventTimeMs: number = inferTimeFromExpirationTimeWithSuspenseConfig( + mostRecentEventTime, + suspenseConfig, + ); + const timeElapsed = currentTimeMs - eventTimeMs; + if (timeElapsed <= busyDelayMs) { + // If we haven't yet waited longer than the initial delay, we don't + // have to wait any additional time. + return 0; + } + const msUntilTimeout = busyDelayMs + busyMinDurationMs - timeElapsed; + // This is the value that is passed to `setTimeout`. + return msUntilTimeout; +} + +function checkForNestedUpdates() { + if (nestedUpdateCount > NESTED_UPDATE_LIMIT) { + nestedUpdateCount = 0; + rootWithNestedUpdates = null; + invariant( + false, + 'Maximum update depth exceeded. This can happen when a component ' + + 'repeatedly calls setState inside componentWillUpdate or ' + + 'componentDidUpdate. React limits the number of nested updates to ' + + 'prevent infinite loops.', + ); + } + + if (__DEV__) { + if (nestedPassiveUpdateCount > NESTED_PASSIVE_UPDATE_LIMIT) { + nestedPassiveUpdateCount = 0; + console.error( + 'Maximum update depth exceeded. This can happen when a component ' + + "calls setState inside useEffect, but useEffect either doesn't " + + 'have a dependency array, or one of the dependencies changes on ' + + 'every render.', + ); + } + } +} + +function flushRenderPhaseStrictModeWarningsInDEV() { + if (__DEV__) { + ReactStrictModeWarnings.flushLegacyContextWarning(); + + if (warnAboutDeprecatedLifecycles) { + ReactStrictModeWarnings.flushPendingUnsafeLifecycleWarnings(); + } + } +} + +let didWarnStateUpdateForUnmountedComponent: Set | null = null; +function warnAboutUpdateOnUnmountedFiberInDEV(fiber) { + if (__DEV__) { + const tag = fiber.tag; + if ( + tag !== HostRoot && + tag !== ClassComponent && + tag !== FunctionComponent && + tag !== ForwardRef && + tag !== MemoComponent && + tag !== SimpleMemoComponent && + tag !== Block + ) { + // Only warn for user-defined components, not internal ones like Suspense. + return; + } + + if ( + deferPassiveEffectCleanupDuringUnmount && + runAllPassiveEffectDestroysBeforeCreates + ) { + // If there are pending passive effects unmounts for this Fiber, + // we can assume that they would have prevented this update. + if (pendingPassiveHookEffectsUnmount.indexOf(fiber) >= 0) { + return; + } + } + + // We show the whole stack but dedupe on the top component's name because + // the problematic code almost always lies inside that component. + const componentName = getComponentName(fiber.type) || 'ReactComponent'; + if (didWarnStateUpdateForUnmountedComponent !== null) { + if (didWarnStateUpdateForUnmountedComponent.has(componentName)) { + return; + } + didWarnStateUpdateForUnmountedComponent.add(componentName); + } else { + didWarnStateUpdateForUnmountedComponent = new Set([componentName]); + } + + if (isFlushingPassiveEffects) { + // Do not warn if we are currently flushing passive effects! + // + // React can't directly detect a memory leak, but there are some clues that warn about one. + // One of these clues is when an unmounted React component tries to update its state. + // For example, if a component forgets to remove an event listener when unmounting, + // that listener may be called later and try to update state, + // at which point React would warn about the potential leak. + // + // Warning signals are the most useful when they're strong. + // (So we should avoid false positive warnings.) + // Updating state from within an effect cleanup function is sometimes a necessary pattern, e.g.: + // 1. Updating an ancestor that a component had registered itself with on mount. + // 2. Resetting state when a component is hidden after going offscreen. + } else { + console.error( + "Can't perform a React state update on an unmounted component. This " + + 'is a no-op, but it indicates a memory leak in your application. To ' + + 'fix, cancel all subscriptions and asynchronous tasks in %s.%s', + tag === ClassComponent + ? 'the componentWillUnmount method' + : 'a useEffect cleanup function', + getStackByFiberInDevAndProd(fiber), + ); + } + } +} + +let beginWork; +if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { + const dummyFiber = null; + beginWork = (current, unitOfWork, expirationTime) => { + // If a component throws an error, we replay it again in a synchronously + // dispatched event, so that the debugger will treat it as an uncaught + // error See ReactErrorUtils for more information. + + // Before entering the begin phase, copy the work-in-progress onto a dummy + // fiber. If beginWork throws, we'll use this to reset the state. + const originalWorkInProgressCopy = assignFiberPropertiesInDEV( + dummyFiber, + unitOfWork, + ); + try { + return originalBeginWork(current, unitOfWork, expirationTime); + } catch (originalError) { + if ( + originalError !== null && + typeof originalError === 'object' && + typeof originalError.then === 'function' + ) { + // Don't replay promises. Treat everything else like an error. + throw originalError; + } + + // Keep this code in sync with handleError; any changes here must have + // corresponding changes there. + resetContextDependencies(); + resetHooksAfterThrow(); + // Don't reset current debug fiber, since we're about to work on the + // same fiber again. + + // Unwind the failed stack frame + unwindInterruptedWork(unitOfWork); + + // Restore the original properties of the fiber. + assignFiberPropertiesInDEV(unitOfWork, originalWorkInProgressCopy); + + if (enableProfilerTimer && unitOfWork.mode & ProfileMode) { + // Reset the profiler timer. + startProfilerTimer(unitOfWork); + } + + // Run beginWork again. + invokeGuardedCallback( + null, + originalBeginWork, + null, + current, + unitOfWork, + expirationTime, + ); + + if (hasCaughtError()) { + const replayError = clearCaughtError(); + // `invokeGuardedCallback` sometimes sets an expando `_suppressLogging`. + // Rethrow this error instead of the original one. + throw replayError; + } else { + // This branch is reachable if the render phase is impure. + throw originalError; + } + } + }; +} else { + beginWork = originalBeginWork; +} + +let didWarnAboutUpdateInRender = false; +let didWarnAboutUpdateInRenderForAnotherComponent; +if (__DEV__) { + didWarnAboutUpdateInRenderForAnotherComponent = new Set(); +} + +function warnAboutRenderPhaseUpdatesInDEV(fiber) { + if (__DEV__) { + if ( + ReactCurrentDebugFiberIsRenderingInDEV && + (executionContext & RenderContext) !== NoContext && + !getIsUpdatingOpaqueValueInRenderPhaseInDEV() + ) { + switch (fiber.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: { + const renderingComponentName = + (workInProgress && getComponentName(workInProgress.type)) || + 'Unknown'; + // Dedupe by the rendering component because it's the one that needs to be fixed. + const dedupeKey = renderingComponentName; + if (!didWarnAboutUpdateInRenderForAnotherComponent.has(dedupeKey)) { + didWarnAboutUpdateInRenderForAnotherComponent.add(dedupeKey); + const setStateComponentName = + getComponentName(fiber.type) || 'Unknown'; + console.error( + 'Cannot update a component (`%s`) while rendering a ' + + 'different component (`%s`). To locate the bad setState() call inside `%s`, ' + + 'follow the stack trace as described in https://fb.me/setstate-in-render', + setStateComponentName, + renderingComponentName, + renderingComponentName, + ); + } + break; + } + case ClassComponent: { + if (!didWarnAboutUpdateInRender) { + console.error( + 'Cannot update during an existing state transition (such as ' + + 'within `render`). Render methods should be a pure ' + + 'function of props and state.', + ); + didWarnAboutUpdateInRender = true; + } + break; + } + } + } + } +} + +// a 'shared' variable that changes when act() opens/closes in tests. +export const IsThisRendererActing = {current: (false: boolean)}; + +export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void { + if (__DEV__) { + if ( + warnsIfNotActing === true && + IsSomeRendererActing.current === true && + IsThisRendererActing.current !== true + ) { + console.error( + "It looks like you're using the wrong act() around your test interactions.\n" + + 'Be sure to use the matching version of act() corresponding to your renderer:\n\n' + + '// for react-dom:\n' + + // Break up imports to avoid accidentally parsing them as dependencies. + 'import {act} fr' + + "om 'react-dom/test-utils';\n" + + '// ...\n' + + 'act(() => ...);\n\n' + + '// for react-test-renderer:\n' + + // Break up imports to avoid accidentally parsing them as dependencies. + 'import TestRenderer fr' + + "om react-test-renderer';\n" + + 'const {act} = TestRenderer;\n' + + '// ...\n' + + 'act(() => ...);' + + '%s', + getStackByFiberInDevAndProd(fiber), + ); + } + } +} + +export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void { + if (__DEV__) { + if ( + warnsIfNotActing === true && + (fiber.mode & StrictMode) !== NoMode && + IsSomeRendererActing.current === false && + IsThisRendererActing.current === false + ) { + console.error( + 'An update to %s ran an effect, but was not wrapped in act(...).\n\n' + + 'When testing, code that causes React state updates should be ' + + 'wrapped into act(...):\n\n' + + 'act(() => {\n' + + ' /* fire events that update state */\n' + + '});\n' + + '/* assert on the output */\n\n' + + "This ensures that you're testing the behavior the user would see " + + 'in the browser.' + + ' Learn more at https://fb.me/react-wrap-tests-with-act' + + '%s', + getComponentName(fiber.type), + getStackByFiberInDevAndProd(fiber), + ); + } + } +} + +function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void { + if (__DEV__) { + if ( + warnsIfNotActing === true && + executionContext === NoContext && + IsSomeRendererActing.current === false && + IsThisRendererActing.current === false + ) { + console.error( + 'An update to %s inside a test was not wrapped in act(...).\n\n' + + 'When testing, code that causes React state updates should be ' + + 'wrapped into act(...):\n\n' + + 'act(() => {\n' + + ' /* fire events that update state */\n' + + '});\n' + + '/* assert on the output */\n\n' + + "This ensures that you're testing the behavior the user would see " + + 'in the browser.' + + ' Learn more at https://fb.me/react-wrap-tests-with-act' + + '%s', + getComponentName(fiber.type), + getStackByFiberInDevAndProd(fiber), + ); + } + } +} + +export const warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDEV; + +// In tests, we want to enforce a mocked scheduler. +let didWarnAboutUnmockedScheduler = false; +// TODO Before we release concurrent mode, revisit this and decide whether a mocked +// scheduler is the actual recommendation. The alternative could be a testing build, +// a new lib, or whatever; we dunno just yet. This message is for early adopters +// to get their tests right. + +export function warnIfUnmockedScheduler(fiber: Fiber) { + if (__DEV__) { + if ( + didWarnAboutUnmockedScheduler === false && + Scheduler.unstable_flushAllWithoutAsserting === undefined + ) { + if (fiber.mode & BlockingMode || fiber.mode & ConcurrentMode) { + didWarnAboutUnmockedScheduler = true; + console.error( + 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' + + 'to guarantee consistent behaviour across tests and browsers. ' + + 'For example, with jest: \n' + + // Break up requires to avoid accidentally parsing them as dependencies. + "jest.mock('scheduler', () => require" + + "('scheduler/unstable_mock'));\n\n" + + 'For more info, visit https://fb.me/react-mock-scheduler', + ); + } else if (warnAboutUnmockedScheduler === true) { + didWarnAboutUnmockedScheduler = true; + console.error( + 'Starting from React v17, the "scheduler" module will need to be mocked ' + + 'to guarantee consistent behaviour across tests and browsers. ' + + 'For example, with jest: \n' + + // Break up requires to avoid accidentally parsing them as dependencies. + "jest.mock('scheduler', () => require" + + "('scheduler/unstable_mock'));\n\n" + + 'For more info, visit https://fb.me/react-mock-scheduler', + ); + } + } + } +} + +function computeThreadID(root, expirationTime) { + // Interaction threads are unique per root and expiration time. + return expirationTime * 1000 + root.interactionThreadID; +} + +export function markSpawnedWork(expirationTime: ExpirationTime) { + if (!enableSchedulerTracing) { + return; + } + if (spawnedWorkDuringRender === null) { + spawnedWorkDuringRender = [expirationTime]; + } else { + spawnedWorkDuringRender.push(expirationTime); + } +} + +function scheduleInteractions(root, expirationTime, interactions) { + if (!enableSchedulerTracing) { + return; + } + + if (interactions.size > 0) { + const pendingInteractionMap = root.pendingInteractionMap; + const pendingInteractions = pendingInteractionMap.get(expirationTime); + if (pendingInteractions != null) { + interactions.forEach(interaction => { + if (!pendingInteractions.has(interaction)) { + // Update the pending async work count for previously unscheduled interaction. + interaction.__count++; + } + + pendingInteractions.add(interaction); + }); + } else { + pendingInteractionMap.set(expirationTime, new Set(interactions)); + + // Update the pending async work count for the current interactions. + interactions.forEach(interaction => { + interaction.__count++; + }); + } + + const subscriber = __subscriberRef.current; + if (subscriber !== null) { + const threadID = computeThreadID(root, expirationTime); + subscriber.onWorkScheduled(interactions, threadID); + } + } +} + +function schedulePendingInteractions(root, expirationTime) { + // This is called when work is scheduled on a root. + // It associates the current interactions with the newly-scheduled expiration. + // They will be restored when that expiration is later committed. + if (!enableSchedulerTracing) { + return; + } + + scheduleInteractions(root, expirationTime, __interactionsRef.current); +} + +function startWorkOnPendingInteractions(root, expirationTime) { + // This is called when new work is started on a root. + if (!enableSchedulerTracing) { + return; + } + + // Determine which interactions this batch of work currently includes, So that + // we can accurately attribute time spent working on it, And so that cascading + // work triggered during the render phase will be associated with it. + const interactions: Set = new Set(); + root.pendingInteractionMap.forEach( + (scheduledInteractions, scheduledExpirationTime) => { + if (scheduledExpirationTime >= expirationTime) { + scheduledInteractions.forEach(interaction => + interactions.add(interaction), + ); + } + }, + ); + + // Store the current set of interactions on the FiberRoot for a few reasons: + // We can re-use it in hot functions like performConcurrentWorkOnRoot() + // without having to recalculate it. We will also use it in commitWork() to + // pass to any Profiler onRender() hooks. This also provides DevTools with a + // way to access it when the onCommitRoot() hook is called. + root.memoizedInteractions = interactions; + + if (interactions.size > 0) { + const subscriber = __subscriberRef.current; + if (subscriber !== null) { + const threadID = computeThreadID(root, expirationTime); + try { + subscriber.onWorkStarted(interactions, threadID); + } catch (error) { + // If the subscriber throws, rethrow it in a separate task + scheduleCallback(ImmediatePriority, () => { + throw error; + }); + } + } + } +} + +function finishPendingInteractions(root, committedExpirationTime) { + if (!enableSchedulerTracing) { + return; + } + + const earliestRemainingTimeAfterCommit = root.firstPendingTime; + + let subscriber; + + try { + subscriber = __subscriberRef.current; + if (subscriber !== null && root.memoizedInteractions.size > 0) { + const threadID = computeThreadID(root, committedExpirationTime); + subscriber.onWorkStopped(root.memoizedInteractions, threadID); + } + } catch (error) { + // If the subscriber throws, rethrow it in a separate task + scheduleCallback(ImmediatePriority, () => { + throw error; + }); + } finally { + // Clear completed interactions from the pending Map. + // Unless the render was suspended or cascading work was scheduled, + // In which case– leave pending interactions until the subsequent render. + const pendingInteractionMap = root.pendingInteractionMap; + pendingInteractionMap.forEach( + (scheduledInteractions, scheduledExpirationTime) => { + // Only decrement the pending interaction count if we're done. + // If there's still work at the current priority, + // That indicates that we are waiting for suspense data. + if (scheduledExpirationTime > earliestRemainingTimeAfterCommit) { + pendingInteractionMap.delete(scheduledExpirationTime); + + scheduledInteractions.forEach(interaction => { + interaction.__count--; + + if (subscriber !== null && interaction.__count === 0) { + try { + subscriber.onInteractionScheduledWorkCompleted(interaction); + } catch (error) { + // If the subscriber throws, rethrow it in a separate task + scheduleCallback(ImmediatePriority, () => { + throw error; + }); + } + } + }); + } + }, + ); + } +} diff --git a/packages/react-reconciler/src/ReactMutableSource.new.js b/packages/react-reconciler/src/ReactMutableSource.new.js new file mode 100644 index 0000000000000..be7561ca2c21c --- /dev/null +++ b/packages/react-reconciler/src/ReactMutableSource.new.js @@ -0,0 +1,128 @@ +/** + * 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 {ExpirationTime} from './ReactFiberExpirationTime'; +import type {FiberRoot} from './ReactInternalTypes'; +import type {MutableSource, MutableSourceVersion} from 'shared/ReactTypes'; + +import {isPrimaryRenderer} from './ReactFiberHostConfig'; +import {NoWork} from './ReactFiberExpirationTime'; + +// Work in progress version numbers only apply to a single render, +// and should be reset before starting a new render. +// This tracks which mutable sources need to be reset after a render. +const workInProgressPrimarySources: Array> = []; +const workInProgressSecondarySources: Array> = []; + +let rendererSigil; +if (__DEV__) { + // Used to detect multiple renderers using the same mutable source. + rendererSigil = {}; +} + +export function clearPendingUpdates( + root: FiberRoot, + expirationTime: ExpirationTime, +): void { + if (expirationTime <= root.mutableSourceLastPendingUpdateTime) { + // All updates for this source have been processed. + root.mutableSourceLastPendingUpdateTime = NoWork; + } +} + +export function getLastPendingExpirationTime(root: FiberRoot): ExpirationTime { + return root.mutableSourceLastPendingUpdateTime; +} + +export function setPendingExpirationTime( + root: FiberRoot, + expirationTime: ExpirationTime, +): void { + const mutableSourceLastPendingUpdateTime = + root.mutableSourceLastPendingUpdateTime; + if ( + mutableSourceLastPendingUpdateTime === NoWork || + expirationTime < mutableSourceLastPendingUpdateTime + ) { + root.mutableSourceLastPendingUpdateTime = expirationTime; + } +} + +export function markSourceAsDirty(mutableSource: MutableSource): void { + if (isPrimaryRenderer) { + workInProgressPrimarySources.push(mutableSource); + } else { + workInProgressSecondarySources.push(mutableSource); + } +} + +export function resetWorkInProgressVersions(): void { + if (isPrimaryRenderer) { + for (let i = 0; i < workInProgressPrimarySources.length; i++) { + const mutableSource = workInProgressPrimarySources[i]; + mutableSource._workInProgressVersionPrimary = null; + } + workInProgressPrimarySources.length = 0; + } else { + for (let i = 0; i < workInProgressSecondarySources.length; i++) { + const mutableSource = workInProgressSecondarySources[i]; + mutableSource._workInProgressVersionSecondary = null; + } + workInProgressSecondarySources.length = 0; + } +} + +export function getWorkInProgressVersion( + mutableSource: MutableSource, +): null | MutableSourceVersion { + if (isPrimaryRenderer) { + return mutableSource._workInProgressVersionPrimary; + } else { + return mutableSource._workInProgressVersionSecondary; + } +} + +export function setWorkInProgressVersion( + mutableSource: MutableSource, + version: MutableSourceVersion, +): void { + if (isPrimaryRenderer) { + mutableSource._workInProgressVersionPrimary = version; + workInProgressPrimarySources.push(mutableSource); + } else { + mutableSource._workInProgressVersionSecondary = version; + workInProgressSecondarySources.push(mutableSource); + } +} + +export function warnAboutMultipleRenderersDEV( + mutableSource: MutableSource, +): void { + if (__DEV__) { + if (isPrimaryRenderer) { + if (mutableSource._currentPrimaryRenderer == null) { + mutableSource._currentPrimaryRenderer = rendererSigil; + } else if (mutableSource._currentPrimaryRenderer !== rendererSigil) { + console.error( + 'Detected multiple renderers concurrently rendering the ' + + 'same mutable source. This is currently unsupported.', + ); + } + } else { + if (mutableSource._currentSecondaryRenderer == null) { + mutableSource._currentSecondaryRenderer = rendererSigil; + } else if (mutableSource._currentSecondaryRenderer !== rendererSigil) { + console.error( + 'Detected multiple renderers concurrently rendering the ' + + 'same mutable source. This is currently unsupported.', + ); + } + } + } +} diff --git a/packages/react-reconciler/src/ReactProfilerTimer.new.js b/packages/react-reconciler/src/ReactProfilerTimer.new.js new file mode 100644 index 0000000000000..69183f70e6453 --- /dev/null +++ b/packages/react-reconciler/src/ReactProfilerTimer.new.js @@ -0,0 +1,161 @@ +/** + * 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 { + enableProfilerTimer, + enableProfilerCommitHooks, +} from 'shared/ReactFeatureFlags'; +import {Profiler} from './ReactWorkTags'; + +// Intentionally not named imports because Rollup would use dynamic dispatch for +// CommonJS interop named imports. +import * as Scheduler from 'scheduler'; + +const {unstable_now: now} = Scheduler; + +export type ProfilerTimer = { + getCommitTime(): number, + recordCommitTime(): void, + startProfilerTimer(fiber: Fiber): void, + stopProfilerTimerIfRunning(fiber: Fiber): void, + stopProfilerTimerIfRunningAndRecordDelta(fiber: Fiber): void, + ... +}; + +let commitTime: number = 0; +let layoutEffectStartTime: number = -1; +let profilerStartTime: number = -1; +let passiveEffectStartTime: number = -1; + +function getCommitTime(): number { + return commitTime; +} + +function recordCommitTime(): void { + if (!enableProfilerTimer) { + return; + } + commitTime = now(); +} + +function startProfilerTimer(fiber: Fiber): void { + if (!enableProfilerTimer) { + return; + } + + profilerStartTime = now(); + + if (((fiber.actualStartTime: any): number) < 0) { + fiber.actualStartTime = now(); + } +} + +function stopProfilerTimerIfRunning(fiber: Fiber): void { + if (!enableProfilerTimer) { + return; + } + profilerStartTime = -1; +} + +function stopProfilerTimerIfRunningAndRecordDelta( + fiber: Fiber, + overrideBaseTime: boolean, +): void { + if (!enableProfilerTimer) { + return; + } + + if (profilerStartTime >= 0) { + const elapsedTime = now() - profilerStartTime; + fiber.actualDuration += elapsedTime; + if (overrideBaseTime) { + fiber.selfBaseDuration = elapsedTime; + } + profilerStartTime = -1; + } +} + +function recordLayoutEffectDuration(fiber: Fiber): void { + if (!enableProfilerTimer || !enableProfilerCommitHooks) { + return; + } + + if (layoutEffectStartTime >= 0) { + const elapsedTime = now() - layoutEffectStartTime; + + layoutEffectStartTime = -1; + + // Store duration on the next nearest Profiler ancestor. + let parentFiber = fiber.return; + while (parentFiber !== null) { + if (parentFiber.tag === Profiler) { + const parentStateNode = parentFiber.stateNode; + parentStateNode.effectDuration += elapsedTime; + break; + } + parentFiber = parentFiber.return; + } + } +} + +function recordPassiveEffectDuration(fiber: Fiber): void { + if (!enableProfilerTimer || !enableProfilerCommitHooks) { + return; + } + + if (passiveEffectStartTime >= 0) { + const elapsedTime = now() - passiveEffectStartTime; + + passiveEffectStartTime = -1; + + // Store duration on the next nearest Profiler ancestor. + let parentFiber = fiber.return; + while (parentFiber !== null) { + if (parentFiber.tag === Profiler) { + const parentStateNode = parentFiber.stateNode; + if (parentStateNode !== null) { + // Detached fibers have their state node cleared out. + // In this case, the return pointer is also cleared out, + // so we won't be able to report the time spent in this Profiler's subtree. + parentStateNode.passiveEffectDuration += elapsedTime; + } + break; + } + parentFiber = parentFiber.return; + } + } +} + +function startLayoutEffectTimer(): void { + if (!enableProfilerTimer || !enableProfilerCommitHooks) { + return; + } + layoutEffectStartTime = now(); +} + +function startPassiveEffectTimer(): void { + if (!enableProfilerTimer || !enableProfilerCommitHooks) { + return; + } + passiveEffectStartTime = now(); +} + +export { + getCommitTime, + recordCommitTime, + recordLayoutEffectDuration, + recordPassiveEffectDuration, + startLayoutEffectTimer, + startPassiveEffectTimer, + startProfilerTimer, + stopProfilerTimerIfRunning, + stopProfilerTimerIfRunningAndRecordDelta, +}; diff --git a/packages/react-reconciler/src/ReactStrictModeWarnings.new.js b/packages/react-reconciler/src/ReactStrictModeWarnings.new.js new file mode 100644 index 0000000000000..e7a311b049fac --- /dev/null +++ b/packages/react-reconciler/src/ReactStrictModeWarnings.new.js @@ -0,0 +1,366 @@ +/** + * 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 {getStackByFiberInDevAndProd} from './ReactFiberComponentStack'; + +import getComponentName from 'shared/getComponentName'; +import {StrictMode} from './ReactTypeOfMode'; + +type FiberArray = Array; +type FiberToFiberComponentsMap = Map; + +const ReactStrictModeWarnings = { + recordUnsafeLifecycleWarnings(fiber: Fiber, instance: any): void {}, + flushPendingUnsafeLifecycleWarnings(): void {}, + recordLegacyContextWarning(fiber: Fiber, instance: any): void {}, + flushLegacyContextWarning(): void {}, + discardPendingWarnings(): void {}, +}; + +if (__DEV__) { + const findStrictRoot = (fiber: Fiber): Fiber | null => { + let maybeStrictRoot = null; + + let node = fiber; + while (node !== null) { + if (node.mode & StrictMode) { + maybeStrictRoot = node; + } + node = node.return; + } + + return maybeStrictRoot; + }; + + const setToSortedString = set => { + const array = []; + set.forEach(value => { + array.push(value); + }); + return array.sort().join(', '); + }; + + let pendingComponentWillMountWarnings: Array = []; + let pendingUNSAFE_ComponentWillMountWarnings: Array = []; + let pendingComponentWillReceivePropsWarnings: Array = []; + let pendingUNSAFE_ComponentWillReceivePropsWarnings: Array = []; + let pendingComponentWillUpdateWarnings: Array = []; + let pendingUNSAFE_ComponentWillUpdateWarnings: Array = []; + + // Tracks components we have already warned about. + const didWarnAboutUnsafeLifecycles = new Set(); + + ReactStrictModeWarnings.recordUnsafeLifecycleWarnings = ( + fiber: Fiber, + instance: any, + ) => { + // Dedup strategy: Warn once per component. + if (didWarnAboutUnsafeLifecycles.has(fiber.type)) { + return; + } + + if ( + typeof instance.componentWillMount === 'function' && + // Don't warn about react-lifecycles-compat polyfilled components. + instance.componentWillMount.__suppressDeprecationWarning !== true + ) { + pendingComponentWillMountWarnings.push(fiber); + } + + if ( + fiber.mode & StrictMode && + typeof instance.UNSAFE_componentWillMount === 'function' + ) { + pendingUNSAFE_ComponentWillMountWarnings.push(fiber); + } + + if ( + typeof instance.componentWillReceiveProps === 'function' && + instance.componentWillReceiveProps.__suppressDeprecationWarning !== true + ) { + pendingComponentWillReceivePropsWarnings.push(fiber); + } + + if ( + fiber.mode & StrictMode && + typeof instance.UNSAFE_componentWillReceiveProps === 'function' + ) { + pendingUNSAFE_ComponentWillReceivePropsWarnings.push(fiber); + } + + if ( + typeof instance.componentWillUpdate === 'function' && + instance.componentWillUpdate.__suppressDeprecationWarning !== true + ) { + pendingComponentWillUpdateWarnings.push(fiber); + } + + if ( + fiber.mode & StrictMode && + typeof instance.UNSAFE_componentWillUpdate === 'function' + ) { + pendingUNSAFE_ComponentWillUpdateWarnings.push(fiber); + } + }; + + ReactStrictModeWarnings.flushPendingUnsafeLifecycleWarnings = () => { + // We do an initial pass to gather component names + const componentWillMountUniqueNames = new Set(); + if (pendingComponentWillMountWarnings.length > 0) { + pendingComponentWillMountWarnings.forEach(fiber => { + componentWillMountUniqueNames.add( + getComponentName(fiber.type) || 'Component', + ); + didWarnAboutUnsafeLifecycles.add(fiber.type); + }); + pendingComponentWillMountWarnings = []; + } + + const UNSAFE_componentWillMountUniqueNames = new Set(); + if (pendingUNSAFE_ComponentWillMountWarnings.length > 0) { + pendingUNSAFE_ComponentWillMountWarnings.forEach(fiber => { + UNSAFE_componentWillMountUniqueNames.add( + getComponentName(fiber.type) || 'Component', + ); + didWarnAboutUnsafeLifecycles.add(fiber.type); + }); + pendingUNSAFE_ComponentWillMountWarnings = []; + } + + const componentWillReceivePropsUniqueNames = new Set(); + if (pendingComponentWillReceivePropsWarnings.length > 0) { + pendingComponentWillReceivePropsWarnings.forEach(fiber => { + componentWillReceivePropsUniqueNames.add( + getComponentName(fiber.type) || 'Component', + ); + didWarnAboutUnsafeLifecycles.add(fiber.type); + }); + + pendingComponentWillReceivePropsWarnings = []; + } + + const UNSAFE_componentWillReceivePropsUniqueNames = new Set(); + if (pendingUNSAFE_ComponentWillReceivePropsWarnings.length > 0) { + pendingUNSAFE_ComponentWillReceivePropsWarnings.forEach(fiber => { + UNSAFE_componentWillReceivePropsUniqueNames.add( + getComponentName(fiber.type) || 'Component', + ); + didWarnAboutUnsafeLifecycles.add(fiber.type); + }); + + pendingUNSAFE_ComponentWillReceivePropsWarnings = []; + } + + const componentWillUpdateUniqueNames = new Set(); + if (pendingComponentWillUpdateWarnings.length > 0) { + pendingComponentWillUpdateWarnings.forEach(fiber => { + componentWillUpdateUniqueNames.add( + getComponentName(fiber.type) || 'Component', + ); + didWarnAboutUnsafeLifecycles.add(fiber.type); + }); + + pendingComponentWillUpdateWarnings = []; + } + + const UNSAFE_componentWillUpdateUniqueNames = new Set(); + if (pendingUNSAFE_ComponentWillUpdateWarnings.length > 0) { + pendingUNSAFE_ComponentWillUpdateWarnings.forEach(fiber => { + UNSAFE_componentWillUpdateUniqueNames.add( + getComponentName(fiber.type) || 'Component', + ); + didWarnAboutUnsafeLifecycles.add(fiber.type); + }); + + pendingUNSAFE_ComponentWillUpdateWarnings = []; + } + + // Finally, we flush all the warnings + // UNSAFE_ ones before the deprecated ones, since they'll be 'louder' + if (UNSAFE_componentWillMountUniqueNames.size > 0) { + const sortedNames = setToSortedString( + UNSAFE_componentWillMountUniqueNames, + ); + console.error( + 'Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. ' + + 'See https://fb.me/react-unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' + + '\nPlease update the following components: %s', + sortedNames, + ); + } + + if (UNSAFE_componentWillReceivePropsUniqueNames.size > 0) { + const sortedNames = setToSortedString( + UNSAFE_componentWillReceivePropsUniqueNames, + ); + console.error( + 'Using UNSAFE_componentWillReceiveProps in strict mode is not recommended ' + + 'and may indicate bugs in your code. ' + + 'See https://fb.me/react-unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + "* If you're updating state whenever props change, " + + 'refactor your code to use memoization techniques or move it to ' + + 'static getDerivedStateFromProps. Learn more at: https://fb.me/react-derived-state\n' + + '\nPlease update the following components: %s', + sortedNames, + ); + } + + if (UNSAFE_componentWillUpdateUniqueNames.size > 0) { + const sortedNames = setToSortedString( + UNSAFE_componentWillUpdateUniqueNames, + ); + console.error( + 'Using UNSAFE_componentWillUpdate in strict mode is not recommended ' + + 'and may indicate bugs in your code. ' + + 'See https://fb.me/react-unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + '\nPlease update the following components: %s', + sortedNames, + ); + } + + if (componentWillMountUniqueNames.size > 0) { + const sortedNames = setToSortedString(componentWillMountUniqueNames); + + console.warn( + 'componentWillMount has been renamed, and is not recommended for use. ' + + 'See https://fb.me/react-unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' + + '* Rename componentWillMount to UNSAFE_componentWillMount to suppress ' + + 'this warning in non-strict mode. In React 17.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: %s', + sortedNames, + ); + } + + if (componentWillReceivePropsUniqueNames.size > 0) { + const sortedNames = setToSortedString( + componentWillReceivePropsUniqueNames, + ); + + console.warn( + 'componentWillReceiveProps has been renamed, and is not recommended for use. ' + + 'See https://fb.me/react-unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + "* If you're updating state whenever props change, refactor your " + + 'code to use memoization techniques or move it to ' + + 'static getDerivedStateFromProps. Learn more at: https://fb.me/react-derived-state\n' + + '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress ' + + 'this warning in non-strict mode. In React 17.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: %s', + sortedNames, + ); + } + + if (componentWillUpdateUniqueNames.size > 0) { + const sortedNames = setToSortedString(componentWillUpdateUniqueNames); + + console.warn( + 'componentWillUpdate has been renamed, and is not recommended for use. ' + + 'See https://fb.me/react-unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress ' + + 'this warning in non-strict mode. In React 17.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: %s', + sortedNames, + ); + } + }; + + let pendingLegacyContextWarning: FiberToFiberComponentsMap = new Map(); + + // Tracks components we have already warned about. + const didWarnAboutLegacyContext = new Set(); + + ReactStrictModeWarnings.recordLegacyContextWarning = ( + fiber: Fiber, + instance: any, + ) => { + const strictRoot = findStrictRoot(fiber); + if (strictRoot === null) { + console.error( + 'Expected to find a StrictMode component in a strict mode tree. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + return; + } + + // Dedup strategy: Warn once per component. + if (didWarnAboutLegacyContext.has(fiber.type)) { + return; + } + + let warningsForRoot = pendingLegacyContextWarning.get(strictRoot); + + if ( + fiber.type.contextTypes != null || + fiber.type.childContextTypes != null || + (instance !== null && typeof instance.getChildContext === 'function') + ) { + if (warningsForRoot === undefined) { + warningsForRoot = []; + pendingLegacyContextWarning.set(strictRoot, warningsForRoot); + } + warningsForRoot.push(fiber); + } + }; + + ReactStrictModeWarnings.flushLegacyContextWarning = () => { + ((pendingLegacyContextWarning: any): FiberToFiberComponentsMap).forEach( + (fiberArray: FiberArray, strictRoot) => { + if (fiberArray.length === 0) { + return; + } + const firstFiber = fiberArray[0]; + + const uniqueNames = new Set(); + fiberArray.forEach(fiber => { + uniqueNames.add(getComponentName(fiber.type) || 'Component'); + didWarnAboutLegacyContext.add(fiber.type); + }); + + const sortedNames = setToSortedString(uniqueNames); + const firstComponentStack = getStackByFiberInDevAndProd(firstFiber); + + console.error( + 'Legacy context API has been detected within a strict-mode tree.' + + '\n\nThe old API will be supported in all 16.x releases, but applications ' + + 'using it should migrate to the new version.' + + '\n\nPlease update the following components: %s' + + '\n\nLearn more about this warning here: https://fb.me/react-legacy-context' + + '%s', + sortedNames, + firstComponentStack, + ); + }, + ); + }; + + ReactStrictModeWarnings.discardPendingWarnings = () => { + pendingComponentWillMountWarnings = []; + pendingUNSAFE_ComponentWillMountWarnings = []; + pendingComponentWillReceivePropsWarnings = []; + pendingUNSAFE_ComponentWillReceivePropsWarnings = []; + pendingComponentWillUpdateWarnings = []; + pendingUNSAFE_ComponentWillUpdateWarnings = []; + pendingLegacyContextWarning = new Map(); + }; +} + +export default ReactStrictModeWarnings; diff --git a/packages/react-reconciler/src/ReactUpdateQueue.new.js b/packages/react-reconciler/src/ReactUpdateQueue.new.js new file mode 100644 index 0000000000000..f85e4dec2af0a --- /dev/null +++ b/packages/react-reconciler/src/ReactUpdateQueue.new.js @@ -0,0 +1,628 @@ +/** + * 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 + */ + +// UpdateQueue is a linked list of prioritized updates. +// +// Like fibers, update queues come in pairs: a current queue, which represents +// the visible state of the screen, and a work-in-progress queue, which can be +// mutated and processed asynchronously before it is committed — a form of +// double buffering. If a work-in-progress render is discarded before finishing, +// we create a new work-in-progress by cloning the current queue. +// +// Both queues share a persistent, singly-linked list structure. To schedule an +// update, we append it to the end of both queues. Each queue maintains a +// pointer to first update in the persistent list that hasn't been processed. +// The work-in-progress pointer always has a position equal to or greater than +// the current queue, since we always work on that one. The current queue's +// pointer is only updated during the commit phase, when we swap in the +// work-in-progress. +// +// For example: +// +// Current pointer: A - B - C - D - E - F +// Work-in-progress pointer: D - E - F +// ^ +// The work-in-progress queue has +// processed more updates than current. +// +// The reason we append to both queues is because otherwise we might drop +// updates without ever processing them. For example, if we only add updates to +// the work-in-progress queue, some updates could be lost whenever a work-in +// -progress render restarts by cloning from current. Similarly, if we only add +// updates to the current queue, the updates will be lost whenever an already +// in-progress queue commits and swaps with the current queue. However, by +// adding to both queues, we guarantee that the update will be part of the next +// work-in-progress. (And because the work-in-progress queue becomes the +// current queue once it commits, there's no danger of applying the same +// update twice.) +// +// Prioritization +// -------------- +// +// Updates are not sorted by priority, but by insertion; new updates are always +// appended to the end of the list. +// +// The priority is still important, though. When processing the update queue +// during the render phase, only the updates with sufficient priority are +// included in the result. If we skip an update because it has insufficient +// priority, it remains in the queue to be processed later, during a lower +// priority render. Crucially, all updates subsequent to a skipped update also +// remain in the queue *regardless of their priority*. That means high priority +// updates are sometimes processed twice, at two separate priorities. We also +// keep track of a base state, that represents the state before the first +// update in the queue is applied. +// +// For example: +// +// Given a base state of '', and the following queue of updates +// +// A1 - B2 - C1 - D2 +// +// where the number indicates the priority, and the update is applied to the +// previous state by appending a letter, React will process these updates as +// two separate renders, one per distinct priority level: +// +// First render, at priority 1: +// Base state: '' +// Updates: [A1, C1] +// Result state: 'AC' +// +// Second render, at priority 2: +// Base state: 'A' <- The base state does not include C1, +// because B2 was skipped. +// Updates: [B2, C1, D2] <- C1 was rebased on top of B2 +// Result state: 'ABCD' +// +// Because we process updates in insertion order, and rebase high priority +// updates when preceding updates are skipped, the final result is deterministic +// regardless of priority. Intermediate state may vary according to system +// resources, but the final state is always the same. + +import type {Fiber} from './ReactInternalTypes'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type {SuspenseConfig} from './ReactFiberSuspenseConfig'; +import type {ReactPriorityLevel} from './ReactInternalTypes'; + +import {NoWork, Sync} from './ReactFiberExpirationTime'; +import { + enterDisallowedContextReadInDEV, + exitDisallowedContextReadInDEV, +} from './ReactFiberNewContext.new'; +import {Callback, ShouldCapture, DidCapture} from './ReactSideEffectTags'; + +import {debugRenderPhaseSideEffectsForStrictMode} from 'shared/ReactFeatureFlags'; + +import {StrictMode} from './ReactTypeOfMode'; +import { + markRenderEventTimeAndConfig, + markUnprocessedUpdateTime, +} from './ReactFiberWorkLoop.new'; + +import invariant from 'shared/invariant'; +import {getCurrentPriorityLevel} from './SchedulerWithReactIntegration.new'; + +import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev'; + +export type Update = {| + expirationTime: ExpirationTime, + suspenseConfig: null | SuspenseConfig, + + tag: 0 | 1 | 2 | 3, + payload: any, + callback: (() => mixed) | null, + + next: Update | null, + + // DEV only + priority?: ReactPriorityLevel, +|}; + +type SharedQueue = {|pending: Update | null|}; + +export type UpdateQueue = {| + baseState: State, + firstBaseUpdate: Update | null, + lastBaseUpdate: Update | null, + shared: SharedQueue, + effects: Array> | null, +|}; + +export const UpdateState = 0; +export const ReplaceState = 1; +export const ForceUpdate = 2; +export const CaptureUpdate = 3; + +// Global state that is reset at the beginning of calling `processUpdateQueue`. +// It should only be read right after calling `processUpdateQueue`, via +// `checkHasForceUpdateAfterProcessing`. +let hasForceUpdate = false; + +let didWarnUpdateInsideUpdate; +let currentlyProcessingQueue; +export let resetCurrentlyProcessingQueue; +if (__DEV__) { + didWarnUpdateInsideUpdate = false; + currentlyProcessingQueue = null; + resetCurrentlyProcessingQueue = () => { + currentlyProcessingQueue = null; + }; +} + +export function initializeUpdateQueue(fiber: Fiber): void { + const queue: UpdateQueue = { + baseState: fiber.memoizedState, + firstBaseUpdate: null, + lastBaseUpdate: null, + shared: { + pending: null, + }, + effects: null, + }; + fiber.updateQueue = queue; +} + +export function cloneUpdateQueue( + current: Fiber, + workInProgress: Fiber, +): void { + // Clone the update queue from current. Unless it's already a clone. + const queue: UpdateQueue = (workInProgress.updateQueue: any); + const currentQueue: UpdateQueue = (current.updateQueue: any); + if (queue === currentQueue) { + const clone: UpdateQueue = { + baseState: currentQueue.baseState, + firstBaseUpdate: currentQueue.firstBaseUpdate, + lastBaseUpdate: currentQueue.lastBaseUpdate, + shared: currentQueue.shared, + effects: currentQueue.effects, + }; + workInProgress.updateQueue = clone; + } +} + +export function createUpdate( + expirationTime: ExpirationTime, + suspenseConfig: null | SuspenseConfig, +): Update<*> { + const update: Update<*> = { + expirationTime, + suspenseConfig, + + tag: UpdateState, + payload: null, + callback: null, + + next: null, + }; + if (__DEV__) { + update.priority = getCurrentPriorityLevel(); + } + return update; +} + +export function enqueueUpdate(fiber: Fiber, update: Update) { + const updateQueue = fiber.updateQueue; + if (updateQueue === null) { + // Only occurs if the fiber has been unmounted. + return; + } + + const sharedQueue: SharedQueue = (updateQueue: any).shared; + const pending = sharedQueue.pending; + if (pending === null) { + // This is the first update. Create a circular list. + update.next = update; + } else { + update.next = pending.next; + pending.next = update; + } + sharedQueue.pending = update; + + if (__DEV__) { + if ( + currentlyProcessingQueue === sharedQueue && + !didWarnUpdateInsideUpdate + ) { + console.error( + 'An update (setState, replaceState, or forceUpdate) was scheduled ' + + 'from inside an update function. Update functions should be pure, ' + + 'with zero side-effects. Consider using componentDidUpdate or a ' + + 'callback.', + ); + didWarnUpdateInsideUpdate = true; + } + } +} + +export function enqueueCapturedUpdate( + workInProgress: Fiber, + capturedUpdate: Update, +) { + // Captured updates are updates that are thrown by a child during the render + // phase. They should be discarded if the render is aborted. Therefore, + // we should only put them on the work-in-progress queue, not the current one. + let queue: UpdateQueue = (workInProgress.updateQueue: any); + + // Check if the work-in-progress queue is a clone. + const current = workInProgress.alternate; + if (current !== null) { + const currentQueue: UpdateQueue = (current.updateQueue: any); + if (queue === currentQueue) { + // The work-in-progress queue is the same as current. This happens when + // we bail out on a parent fiber that then captures an error thrown by + // a child. Since we want to append the update only to the work-in + // -progress queue, we need to clone the updates. We usually clone during + // processUpdateQueue, but that didn't happen in this case because we + // skipped over the parent when we bailed out. + let newFirst = null; + let newLast = null; + const firstBaseUpdate = queue.firstBaseUpdate; + if (firstBaseUpdate !== null) { + // Loop through the updates and clone them. + let update = firstBaseUpdate; + do { + const clone: Update = { + expirationTime: update.expirationTime, + suspenseConfig: update.suspenseConfig, + + tag: update.tag, + payload: update.payload, + callback: update.callback, + + next: null, + }; + if (newLast === null) { + newFirst = newLast = clone; + } else { + newLast.next = clone; + newLast = clone; + } + update = update.next; + } while (update !== null); + + // Append the captured update the end of the cloned list. + if (newLast === null) { + newFirst = newLast = capturedUpdate; + } else { + newLast.next = capturedUpdate; + newLast = capturedUpdate; + } + } else { + // There are no base updates. + newFirst = newLast = capturedUpdate; + } + queue = { + baseState: currentQueue.baseState, + firstBaseUpdate: newFirst, + lastBaseUpdate: newLast, + shared: currentQueue.shared, + effects: currentQueue.effects, + }; + workInProgress.updateQueue = queue; + return; + } + } + + // Append the update to the end of the list. + const lastBaseUpdate = queue.lastBaseUpdate; + if (lastBaseUpdate === null) { + queue.firstBaseUpdate = capturedUpdate; + } else { + lastBaseUpdate.next = capturedUpdate; + } + queue.lastBaseUpdate = capturedUpdate; +} + +function getStateFromUpdate( + workInProgress: Fiber, + queue: UpdateQueue, + update: Update, + prevState: State, + nextProps: any, + instance: any, +): any { + switch (update.tag) { + case ReplaceState: { + const payload = update.payload; + if (typeof payload === 'function') { + // Updater function + if (__DEV__) { + enterDisallowedContextReadInDEV(); + } + const nextState = payload.call(instance, prevState, nextProps); + if (__DEV__) { + if ( + debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode + ) { + disableLogs(); + try { + payload.call(instance, prevState, nextProps); + } finally { + reenableLogs(); + } + } + exitDisallowedContextReadInDEV(); + } + return nextState; + } + // State object + return payload; + } + case CaptureUpdate: { + workInProgress.effectTag = + (workInProgress.effectTag & ~ShouldCapture) | DidCapture; + } + // Intentional fallthrough + case UpdateState: { + const payload = update.payload; + let partialState; + if (typeof payload === 'function') { + // Updater function + if (__DEV__) { + enterDisallowedContextReadInDEV(); + } + partialState = payload.call(instance, prevState, nextProps); + if (__DEV__) { + if ( + debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode + ) { + disableLogs(); + try { + payload.call(instance, prevState, nextProps); + } finally { + reenableLogs(); + } + } + exitDisallowedContextReadInDEV(); + } + } else { + // Partial state object + partialState = payload; + } + if (partialState === null || partialState === undefined) { + // Null and undefined are treated as no-ops. + return prevState; + } + // Merge the partial state and the previous state. + return Object.assign({}, prevState, partialState); + } + case ForceUpdate: { + hasForceUpdate = true; + return prevState; + } + } + return prevState; +} + +export function processUpdateQueue( + workInProgress: Fiber, + props: any, + instance: any, + renderExpirationTime: ExpirationTime, +): void { + // This is always non-null on a ClassComponent or HostRoot + const queue: UpdateQueue = (workInProgress.updateQueue: any); + + hasForceUpdate = false; + + if (__DEV__) { + currentlyProcessingQueue = queue.shared; + } + + let firstBaseUpdate = queue.firstBaseUpdate; + let lastBaseUpdate = queue.lastBaseUpdate; + + // Check if there are pending updates. If so, transfer them to the base queue. + let pendingQueue = queue.shared.pending; + if (pendingQueue !== null) { + queue.shared.pending = null; + + // The pending queue is circular. Disconnect the pointer between first + // and last so that it's non-circular. + const lastPendingUpdate = pendingQueue; + const firstPendingUpdate = lastPendingUpdate.next; + lastPendingUpdate.next = null; + // Append pending updates to base queue + if (lastBaseUpdate === null) { + firstBaseUpdate = firstPendingUpdate; + } else { + lastBaseUpdate.next = firstPendingUpdate; + } + lastBaseUpdate = lastPendingUpdate; + + // If there's a current queue, and it's different from the base queue, then + // we need to transfer the updates to that queue, too. Because the base + // queue is a singly-linked list with no cycles, we can append to both + // lists and take advantage of structural sharing. + // TODO: Pass `current` as argument + const current = workInProgress.alternate; + if (current !== null) { + // This is always non-null on a ClassComponent or HostRoot + const currentQueue: UpdateQueue = (current.updateQueue: any); + const currentLastBaseUpdate = currentQueue.lastBaseUpdate; + if (currentLastBaseUpdate !== lastBaseUpdate) { + if (currentLastBaseUpdate === null) { + currentQueue.firstBaseUpdate = firstPendingUpdate; + } else { + currentLastBaseUpdate.next = firstPendingUpdate; + } + currentQueue.lastBaseUpdate = lastPendingUpdate; + } + } + } + + // These values may change as we process the queue. + if (firstBaseUpdate !== null) { + // Iterate through the list of updates to compute the result. + let newState = queue.baseState; + let newExpirationTime = NoWork; + + let newBaseState = null; + let newFirstBaseUpdate = null; + let newLastBaseUpdate = null; + + let update = firstBaseUpdate; + do { + const updateExpirationTime = update.expirationTime; + if (updateExpirationTime < renderExpirationTime) { + // Priority is insufficient. Skip this update. If this is the first + // skipped update, the previous update/state is the new base + // update/state. + const clone: Update = { + expirationTime: update.expirationTime, + suspenseConfig: update.suspenseConfig, + + tag: update.tag, + payload: update.payload, + callback: update.callback, + + next: null, + }; + if (newLastBaseUpdate === null) { + newFirstBaseUpdate = newLastBaseUpdate = clone; + newBaseState = newState; + } else { + newLastBaseUpdate = newLastBaseUpdate.next = clone; + } + // Update the remaining priority in the queue. + if (updateExpirationTime > newExpirationTime) { + newExpirationTime = updateExpirationTime; + } + } else { + // This update does have sufficient priority. + + if (newLastBaseUpdate !== null) { + const clone: Update = { + expirationTime: Sync, // This update is going to be committed so we never want uncommit it. + suspenseConfig: update.suspenseConfig, + + tag: update.tag, + payload: update.payload, + callback: update.callback, + + next: null, + }; + newLastBaseUpdate = newLastBaseUpdate.next = clone; + } + + // Mark the event time of this update as relevant to this render pass. + // TODO: This should ideally use the true event time of this update rather than + // its priority which is a derived and not reverseable value. + // TODO: We should skip this update if it was already committed but currently + // we have no way of detecting the difference between a committed and suspended + // update here. + markRenderEventTimeAndConfig( + updateExpirationTime, + update.suspenseConfig, + ); + + // Process this update. + newState = getStateFromUpdate( + workInProgress, + queue, + update, + newState, + props, + instance, + ); + const callback = update.callback; + if (callback !== null) { + workInProgress.effectTag |= Callback; + const effects = queue.effects; + if (effects === null) { + queue.effects = [update]; + } else { + effects.push(update); + } + } + } + update = update.next; + if (update === null) { + pendingQueue = queue.shared.pending; + if (pendingQueue === null) { + break; + } else { + // An update was scheduled from inside a reducer. Add the new + // pending updates to the end of the list and keep processing. + const lastPendingUpdate = pendingQueue; + // Intentionally unsound. Pending updates form a circular list, but we + // unravel them when transferring them to the base queue. + const firstPendingUpdate = ((lastPendingUpdate.next: any): Update); + lastPendingUpdate.next = null; + update = firstPendingUpdate; + queue.lastBaseUpdate = lastPendingUpdate; + queue.shared.pending = null; + } + } + } while (true); + + if (newLastBaseUpdate === null) { + newBaseState = newState; + } + + queue.baseState = ((newBaseState: any): State); + queue.firstBaseUpdate = newFirstBaseUpdate; + queue.lastBaseUpdate = newLastBaseUpdate; + + // Set the remaining expiration time to be whatever is remaining in the queue. + // This should be fine because the only two other things that contribute to + // expiration time are props and context. We're already in the middle of the + // begin phase by the time we start processing the queue, so we've already + // dealt with the props. Context in components that specify + // shouldComponentUpdate is tricky; but we'll have to account for + // that regardless. + markUnprocessedUpdateTime(newExpirationTime); + workInProgress.expirationTime = newExpirationTime; + workInProgress.memoizedState = newState; + } + + if (__DEV__) { + currentlyProcessingQueue = null; + } +} + +function callCallback(callback, context) { + invariant( + typeof callback === 'function', + 'Invalid argument passed as callback. Expected a function. Instead ' + + 'received: %s', + callback, + ); + callback.call(context); +} + +export function resetHasForceUpdateBeforeProcessing() { + hasForceUpdate = false; +} + +export function checkHasForceUpdateAfterProcessing(): boolean { + return hasForceUpdate; +} + +export function commitUpdateQueue( + finishedWork: Fiber, + finishedQueue: UpdateQueue, + instance: any, +): void { + // Commit the effects + const effects = finishedQueue.effects; + finishedQueue.effects = null; + if (effects !== null) { + for (let i = 0; i < effects.length; i++) { + const effect = effects[i]; + const callback = effect.callback; + if (callback !== null) { + effect.callback = null; + callCallback(callback, instance); + } + } + } +} diff --git a/packages/react-reconciler/src/SchedulerWithReactIntegration.new.js b/packages/react-reconciler/src/SchedulerWithReactIntegration.new.js new file mode 100644 index 0000000000000..5618a9c78c96d --- /dev/null +++ b/packages/react-reconciler/src/SchedulerWithReactIntegration.new.js @@ -0,0 +1,201 @@ +/** + * 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 {ReactPriorityLevel} from './ReactInternalTypes'; + +// Intentionally not named imports because Rollup would use dynamic dispatch for +// CommonJS interop named imports. +import * as Scheduler from 'scheduler'; +import {__interactionsRef} from 'scheduler/tracing'; +import {enableSchedulerTracing} from 'shared/ReactFeatureFlags'; +import invariant from 'shared/invariant'; + +const { + unstable_runWithPriority: Scheduler_runWithPriority, + unstable_scheduleCallback: Scheduler_scheduleCallback, + unstable_cancelCallback: Scheduler_cancelCallback, + unstable_shouldYield: Scheduler_shouldYield, + unstable_requestPaint: Scheduler_requestPaint, + unstable_now: Scheduler_now, + unstable_getCurrentPriorityLevel: Scheduler_getCurrentPriorityLevel, + unstable_ImmediatePriority: Scheduler_ImmediatePriority, + unstable_UserBlockingPriority: Scheduler_UserBlockingPriority, + unstable_NormalPriority: Scheduler_NormalPriority, + unstable_LowPriority: Scheduler_LowPriority, + unstable_IdlePriority: Scheduler_IdlePriority, +} = Scheduler; + +if (enableSchedulerTracing) { + // Provide explicit error message when production+profiling bundle of e.g. + // react-dom is used with production (non-profiling) bundle of + // scheduler/tracing + invariant( + __interactionsRef != null && __interactionsRef.current != null, + 'It is not supported to run the profiling version of a renderer (for ' + + 'example, `react-dom/profiling`) without also replacing the ' + + '`scheduler/tracing` module with `scheduler/tracing-profiling`. Your ' + + 'bundler might have a setting for aliasing both modules. Learn more at ' + + 'http://fb.me/react-profiling', + ); +} + +export type SchedulerCallback = (isSync: boolean) => SchedulerCallback | null; + +type SchedulerCallbackOptions = {timeout?: number, ...}; + +const fakeCallbackNode = {}; + +// Except for NoPriority, these correspond to Scheduler priorities. We use +// ascending numbers so we can compare them like numbers. They start at 90 to +// avoid clashing with Scheduler's priorities. +export const ImmediatePriority: ReactPriorityLevel = 99; +export const UserBlockingPriority: ReactPriorityLevel = 98; +export const NormalPriority: ReactPriorityLevel = 97; +export const LowPriority: ReactPriorityLevel = 96; +export const IdlePriority: ReactPriorityLevel = 95; +// NoPriority is the absence of priority. Also React-only. +export const NoPriority: ReactPriorityLevel = 90; + +export const shouldYield = Scheduler_shouldYield; +export const requestPaint = + // Fall back gracefully if we're running an older version of Scheduler. + Scheduler_requestPaint !== undefined ? Scheduler_requestPaint : () => {}; + +let syncQueue: Array | null = null; +let immediateQueueCallbackNode: mixed | null = null; +let isFlushingSyncQueue: boolean = false; +const initialTimeMs: number = Scheduler_now(); + +// If the initial timestamp is reasonably small, use Scheduler's `now` directly. +// This will be the case for modern browsers that support `performance.now`. In +// older browsers, Scheduler falls back to `Date.now`, which returns a Unix +// timestamp. In that case, subtract the module initialization time to simulate +// the behavior of performance.now and keep our times small enough to fit +// within 32 bits. +// TODO: Consider lifting this into Scheduler. +export const now = + initialTimeMs < 10000 ? Scheduler_now : () => Scheduler_now() - initialTimeMs; + +export function getCurrentPriorityLevel(): ReactPriorityLevel { + switch (Scheduler_getCurrentPriorityLevel()) { + case Scheduler_ImmediatePriority: + return ImmediatePriority; + case Scheduler_UserBlockingPriority: + return UserBlockingPriority; + case Scheduler_NormalPriority: + return NormalPriority; + case Scheduler_LowPriority: + return LowPriority; + case Scheduler_IdlePriority: + return IdlePriority; + default: + invariant(false, 'Unknown priority level.'); + } +} + +function reactPriorityToSchedulerPriority(reactPriorityLevel) { + switch (reactPriorityLevel) { + case ImmediatePriority: + return Scheduler_ImmediatePriority; + case UserBlockingPriority: + return Scheduler_UserBlockingPriority; + case NormalPriority: + return Scheduler_NormalPriority; + case LowPriority: + return Scheduler_LowPriority; + case IdlePriority: + return Scheduler_IdlePriority; + default: + invariant(false, 'Unknown priority level.'); + } +} + +export function runWithPriority( + reactPriorityLevel: ReactPriorityLevel, + fn: () => T, +): T { + const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel); + return Scheduler_runWithPriority(priorityLevel, fn); +} + +export function scheduleCallback( + reactPriorityLevel: ReactPriorityLevel, + callback: SchedulerCallback, + options: SchedulerCallbackOptions | void | null, +) { + const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel); + return Scheduler_scheduleCallback(priorityLevel, callback, options); +} + +export function scheduleSyncCallback(callback: SchedulerCallback) { + // Push this callback into an internal queue. We'll flush these either in + // the next tick, or earlier if something calls `flushSyncCallbackQueue`. + if (syncQueue === null) { + syncQueue = [callback]; + // Flush the queue in the next tick, at the earliest. + immediateQueueCallbackNode = Scheduler_scheduleCallback( + Scheduler_ImmediatePriority, + flushSyncCallbackQueueImpl, + ); + } else { + // Push onto existing queue. Don't need to schedule a callback because + // we already scheduled one when we created the queue. + syncQueue.push(callback); + } + return fakeCallbackNode; +} + +export function cancelCallback(callbackNode: mixed) { + if (callbackNode !== fakeCallbackNode) { + Scheduler_cancelCallback(callbackNode); + } +} + +export function flushSyncCallbackQueue() { + if (immediateQueueCallbackNode !== null) { + const node = immediateQueueCallbackNode; + immediateQueueCallbackNode = null; + Scheduler_cancelCallback(node); + } + flushSyncCallbackQueueImpl(); +} + +function flushSyncCallbackQueueImpl() { + if (!isFlushingSyncQueue && syncQueue !== null) { + // Prevent re-entrancy. + isFlushingSyncQueue = true; + let i = 0; + try { + const isSync = true; + const queue = syncQueue; + runWithPriority(ImmediatePriority, () => { + for (; i < queue.length; i++) { + let callback = queue[i]; + do { + callback = callback(isSync); + } while (callback !== null); + } + }); + syncQueue = null; + } catch (error) { + // If something throws, leave the remaining callbacks on the queue. + if (syncQueue !== null) { + syncQueue = syncQueue.slice(i + 1); + } + // Resume flushing in the next tick + Scheduler_scheduleCallback( + Scheduler_ImmediatePriority, + flushSyncCallbackQueue, + ); + throw error; + } finally { + isFlushingSyncQueue = false; + } + } +}