-
-
Save gaearon/ddbda5845b4dba0d9915e2ed7f7b11e2 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- a/packages/react-reconciler/src/ReactChildFiber.js | |
+++ b/packages/react-reconciler/src/ReactChildFiber.js | |
@@ -43,6 +43,7 @@ import { | |
getCurrentFiberStackInDev, | |
getStackByFiberInDevAndProd, | |
} from './ReactCurrentFiber'; | |
+import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading'; | |
import {StrictMode} from './ReactTypeOfMode'; | |
let didWarnAboutMaps; | |
@@ -378,7 +379,12 @@ function ChildReconciler(shouldTrackSideEffects) { | |
element: ReactElement, | |
expirationTime: ExpirationTime, | |
): Fiber { | |
- if (current !== null && current.elementType === element.type) { | |
+ if ( | |
+ current !== null && | |
+ (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, expirationTime); | |
existing.ref = coerceRef(returnFiber, current, element); | |
@@ -1121,7 +1127,11 @@ function ChildReconciler(shouldTrackSideEffects) { | |
if ( | |
child.tag === Fragment | |
? element.type === REACT_FRAGMENT_TYPE | |
- : child.elementType === element.type | |
+ : 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( | |
diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js | |
index 8ca24a16a..4996971d1 100644 | |
--- a/packages/react-reconciler/src/ReactFiber.js | |
+++ b/packages/react-reconciler/src/ReactFiber.js | |
@@ -43,6 +43,7 @@ import { | |
SuspenseComponent, | |
FunctionComponent, | |
MemoComponent, | |
+ SimpleMemoComponent, | |
LazyComponent, | |
EventComponent, | |
EventTarget, | |
@@ -50,6 +51,11 @@ import { | |
import getComponentName from 'shared/getComponentName'; | |
import {isDevToolsPresent} from './ReactFiberDevToolsHook'; | |
+import { | |
+ resolveClassForHotReloading, | |
+ resolveFunctionForHotReloading, | |
+ resolveForwardRefForHotReloading, | |
+} from './ReactFiberHotReloading'; | |
import {NoWork} from './ReactFiberExpirationTime'; | |
import { | |
NoContext, | |
@@ -215,6 +221,7 @@ export type Fiber = {| | |
_debugSource?: Source | null, | |
_debugOwner?: Fiber | null, | |
_debugIsCurrentlyTiming?: boolean, | |
+ _debugNeedsRemount?: boolean, | |
// Used to verify that the order of hooks does not change between renders. | |
_debugHookTypes?: Array<HookType> | null, | |
@@ -299,6 +306,7 @@ function FiberNode( | |
this._debugSource = null; | |
this._debugOwner = null; | |
this._debugIsCurrentlyTiming = false; | |
+ this._debugNeedsRemount = false; | |
this._debugHookTypes = null; | |
if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') { | |
Object.preventExtensions(this); | |
@@ -431,6 +439,25 @@ export function createWorkInProgress( | |
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; | |
} | |
@@ -463,6 +490,13 @@ export function createFiberFromTypeAndProps( | |
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; | |
@@ -505,6 +539,9 @@ export function createFiberFromTypeAndProps( | |
break getTag; | |
case REACT_FORWARD_REF_TYPE: | |
fiberTag = ForwardRef; | |
+ if (__DEV__) { | |
+ resolvedType = resolveForwardRefForHotReloading(resolvedType); | |
+ } | |
break getTag; | |
case REACT_MEMO_TYPE: | |
fiberTag = MemoComponent; | |
@@ -793,6 +830,7 @@ export function assignFiberPropertiesInDEV( | |
target._debugSource = source._debugSource; | |
target._debugOwner = source._debugOwner; | |
target._debugIsCurrentlyTiming = source._debugIsCurrentlyTiming; | |
+ target._debugNeedsRemount = source._debugNeedsRemount; | |
target._debugHookTypes = source._debugHookTypes; | |
return target; | |
} | |
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js | |
index 8659e010a..6e854aa75 100644 | |
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js | |
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js | |
@@ -70,6 +70,11 @@ import { | |
getCurrentFiberStackInDev, | |
} from './ReactCurrentFiber'; | |
import {startWorkTimer, cancelWorkTimer} from './ReactDebugFiberPerf'; | |
+import { | |
+ resolveFunctionForHotReloading, | |
+ resolveForwardRefForHotReloading, | |
+ resolveClassForHotReloading, | |
+} from './ReactFiberHotReloading'; | |
import { | |
mountChildFibers, | |
@@ -346,18 +351,22 @@ function updateMemoComponent( | |
// 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 = type; | |
+ workInProgress.type = resolvedType; | |
if (__DEV__) { | |
validateFunctionComponentInDev(workInProgress, type); | |
} | |
return updateSimpleMemoComponent( | |
current, | |
workInProgress, | |
- type, | |
+ resolvedType, | |
nextProps, | |
updateExpirationTime, | |
renderExpirationTime, | |
@@ -474,7 +483,9 @@ function updateSimpleMemoComponent( | |
const prevProps = current.memoizedProps; | |
if ( | |
shallowEqual(prevProps, nextProps) && | |
- current.ref === workInProgress.ref | |
+ 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) { | |
@@ -1021,6 +1032,9 @@ function mountLazyComponent( | |
case FunctionComponent: { | |
if (__DEV__) { | |
validateFunctionComponentInDev(workInProgress, Component); | |
+ workInProgress.type = Component = resolveFunctionForHotReloading( | |
+ Component, | |
+ ); | |
} | |
child = updateFunctionComponent( | |
null, | |
@@ -1032,6 +1046,11 @@ function mountLazyComponent( | |
break; | |
} | |
case ClassComponent: { | |
+ if (__DEV__) { | |
+ workInProgress.type = Component = resolveClassForHotReloading( | |
+ Component, | |
+ ); | |
+ } | |
child = updateClassComponent( | |
null, | |
workInProgress, | |
@@ -1042,6 +1061,11 @@ function mountLazyComponent( | |
break; | |
} | |
case ForwardRef: { | |
+ if (__DEV__) { | |
+ workInProgress.type = Component = resolveForwardRefForHotReloading( | |
+ Component, | |
+ ); | |
+ } | |
child = updateForwardRef( | |
null, | |
workInProgress, | |
@@ -2057,6 +2081,68 @@ function bailoutOnAlreadyFinishedWork( | |
} | |
} | |
+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; | |
+ | |
+ // 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, | |
@@ -2064,11 +2150,34 @@ function beginWork( | |
): 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()) { | |
+ 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; | |
@@ -2185,11 +2294,10 @@ function beginWork( | |
switch (workInProgress.tag) { | |
case IndeterminateComponent: { | |
- const elementType = workInProgress.elementType; | |
return mountIndeterminateComponent( | |
current, | |
workInProgress, | |
- elementType, | |
+ workInProgress.type, | |
renderExpirationTime, | |
); | |
} | |
diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js | |
index b5b7f9dd7..c14cf9649 100644 | |
--- a/packages/react-reconciler/src/ReactFiberHooks.js | |
+++ b/packages/react-reconciler/src/ReactFiberHooks.js | |
@@ -185,6 +185,11 @@ let currentHookNameInDev: ?HookType = null; | |
let hookTypesDev: Array<HookType> | 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); | |
@@ -292,6 +297,13 @@ function areHookInputsEqual( | |
nextDeps: Array<mixed>, | |
prevDeps: Array<mixed> | null, | |
) { | |
+ if (__DEV__) { | |
+ if (ignorePreviousDependencies) { | |
+ // Only true when this component is being hot reloaded. | |
+ return false; | |
+ } | |
+ } | |
+ | |
if (prevDeps === null) { | |
if (__DEV__) { | |
warning( | |
@@ -348,6 +360,9 @@ export function renderWithHooks( | |
? ((current._debugHookTypes: any): Array<HookType>) | |
: null; | |
hookTypesUpdateIndexDev = -1; | |
+ // Used for hot reloading: | |
+ ignorePreviousDependencies = | |
+ current !== null && current.type !== workInProgress.type; | |
} | |
// The following should have already been reset | |
diff --git a/packages/react-reconciler/src/ReactFiberHotReloading.js b/packages/react-reconciler/src/ReactFiberHotReloading.js | |
new file mode 100644 | |
index 000000000..dc1960c72 | |
--- /dev/null | |
+++ b/packages/react-reconciler/src/ReactFiberHotReloading.js | |
@@ -0,0 +1,436 @@ | |
+/** | |
+ * 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 './ReactFiber'; | |
+import type {FiberRoot} from './ReactFiberRoot'; | |
+import type {Instance} from './ReactFiberHostConfig'; | |
+ | |
+import { | |
+ flushSync, | |
+ scheduleWork, | |
+ flushPassiveEffects, | |
+} from './ReactFiberScheduler'; | |
+import {Sync} from './ReactFiberExpirationTime'; | |
+import { | |
+ ClassComponent, | |
+ FunctionComponent, | |
+ ForwardRef, | |
+ HostComponent, | |
+ HostPortal, | |
+ HostRoot, | |
+ MemoComponent, | |
+ SimpleMemoComponent, | |
+} from 'shared/ReactWorkTags'; | |
+import { | |
+ REACT_FORWARD_REF_TYPE, | |
+ REACT_MEMO_TYPE, | |
+ REACT_LAZY_TYPE, | |
+} from 'shared/ReactSymbols'; | |
+ | |
+export type Family = {| | |
+ current: any, | |
+|}; | |
+ | |
+export type HotUpdate = {| | |
+ resolveFamily: (any => Family | void) | null, | |
+ staleFamilies: Set<Family>, | |
+ updatedFamilies: Set<Family>, | |
+|}; | |
+ | |
+let resolveFamily: (any => Family | void) | null = null; | |
+// $FlowFixMe Flow gets confused by a WeakSet feature check below. | |
+let failedBoundaries: WeakSet<Fiber> | null = null; | |
+ | |
+export function resolveFunctionForHotReloading(type: any): any { | |
+ if (__DEV__) { | |
+ if (resolveFamily === null) { | |
+ // Hot reloading is disabled. | |
+ return type; | |
+ } | |
+ let 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; | |
+ } | |
+ let 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) { | |
+ // Not hot reloading. | |
+ return; | |
+ } | |
+ if (typeof WeakSet !== 'function') { | |
+ return; | |
+ } | |
+ if (failedBoundaries === null) { | |
+ failedBoundaries = new WeakSet(); | |
+ } | |
+ failedBoundaries.add(fiber); | |
+ } | |
+} | |
+ | |
+export function scheduleHotUpdate(root: FiberRoot, hotUpdate: HotUpdate): void { | |
+ if (__DEV__) { | |
+ // TODO: warn if its identity changes over time? | |
+ resolveFamily = hotUpdate.resolveFamily; | |
+ | |
+ const {staleFamilies, updatedFamilies} = hotUpdate; | |
+ flushPassiveEffects(); | |
+ flushSync(() => { | |
+ scheduleFibersWithFamiliesRecursively( | |
+ root.current, | |
+ updatedFamilies, | |
+ staleFamilies, | |
+ ); | |
+ }); | |
+ } | |
+} | |
+ | |
+function scheduleFibersWithFamiliesRecursively( | |
+ fiber: Fiber, | |
+ updatedFamilies: Set<Family>, | |
+ staleFamilies: Set<Family>, | |
+) { | |
+ 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)) { | |
+ needsRender = true; | |
+ } | |
+ } | |
+ } | |
+ if (failedBoundaries !== null) { | |
+ if ( | |
+ failedBoundaries.has(fiber) || | |
+ (alternate !== null && failedBoundaries.has(alternate)) | |
+ ) { | |
+ needsRemount = true; | |
+ } | |
+ } | |
+ | |
+ if (needsRemount) { | |
+ fiber._debugNeedsRemount = true; | |
+ } | |
+ if (needsRemount || needsRender) { | |
+ scheduleWork(fiber, Sync); | |
+ } | |
+ if (child !== null && !needsRemount) { | |
+ scheduleFibersWithFamiliesRecursively( | |
+ child, | |
+ updatedFamilies, | |
+ staleFamilies, | |
+ ); | |
+ } | |
+ if (sibling !== null) { | |
+ scheduleFibersWithFamiliesRecursively( | |
+ sibling, | |
+ updatedFamilies, | |
+ staleFamilies, | |
+ ); | |
+ } | |
+ } | |
+} | |
+ | |
+export function findHostInstancesForHotUpdate( | |
+ root: FiberRoot, | |
+ families: Array<Family>, | |
+): Set<Instance> { | |
+ 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 findHostInstancesForHotUpdate to be called in production.', | |
+ ); | |
+ } | |
+} | |
+ | |
+function findHostInstancesForMatchingFibersRecursively( | |
+ fiber: Fiber, | |
+ types: Set<any>, | |
+ hostInstances: Set<Instance>, | |
+) { | |
+ 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<Instance>, | |
+): 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<Instance>, | |
+): 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/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js | |
index 77fadd31e..10f7b7662 100644 | |
--- a/packages/react-reconciler/src/ReactFiberReconciler.js | |
+++ b/packages/react-reconciler/src/ReactFiberReconciler.js | |
@@ -63,6 +63,10 @@ import { | |
} from './ReactCurrentFiber'; | |
import {StrictMode} from './ReactTypeOfMode'; | |
import {Sync} from './ReactFiberExpirationTime'; | |
+import { | |
+ scheduleHotUpdate, | |
+ findHostInstancesForHotUpdate, | |
+} from './ReactFiberHotReloading'; | |
type OpaqueRoot = FiberRoot; | |
@@ -434,6 +438,10 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean { | |
return injectInternals({ | |
...devToolsConfig, | |
+ findHostInstancesForHotUpdate: __DEV__ | |
+ ? findHostInstancesForHotUpdate | |
+ : null, | |
+ scheduleHotUpdate: __DEV__ ? scheduleHotUpdate : null, | |
overrideHookState, | |
overrideProps, | |
setSuspenseHandler, | |
diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js | |
index c6c827673..f2ccc813c 100644 | |
--- a/packages/react-reconciler/src/ReactFiberUnwindWork.js | |
+++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js | |
@@ -55,6 +55,7 @@ import { | |
import {logError} from './ReactFiberCommitWork'; | |
import {getStackByFiberInDevAndProd} from './ReactCurrentFiber'; | |
import {popHostContainer, popHostContext} from './ReactFiberHostContext'; | |
+import {markFailedErrorBoundaryForHotReloading} from './ReactFiberHotReloading'; | |
import { | |
isContextProvider as isLegacyContextProvider, | |
popContext as popLegacyContext, | |
@@ -114,6 +115,9 @@ function createClassErrorUpdate( | |
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. | |
@@ -142,6 +146,10 @@ function createClassErrorUpdate( | |
} | |
} | |
}; | |
+ } else if (__DEV__) { | |
+ update.callback = () => { | |
+ markFailedErrorBoundaryForHotReloading(fiber); | |
+ }; | |
} | |
return update; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment