diff --git a/packages/react-fresh/src/ReactFreshBabelPlugin.js b/packages/react-fresh/src/ReactFreshBabelPlugin.js index a93656aaf398a..939d2390839c1 100644 --- a/packages/react-fresh/src/ReactFreshBabelPlugin.js +++ b/packages/react-fresh/src/ReactFreshBabelPlugin.js @@ -223,11 +223,65 @@ export default function(babel) { }; } - function createArgumentsForSignature(node, signature) { + let hasForceResetCommentByFile = new WeakMap(); + + // We let user do /* @hot reset */ to reset state in the whole file. + function hasForceResetComment(path) { + const file = path.hub.file; + let hasForceReset = hasForceResetCommentByFile.get(file); + if (hasForceReset !== undefined) { + return hasForceReset; + } + + hasForceReset = false; + const comments = file.ast.comments; + for (let i = 0; i < comments.length; i++) { + const cmt = comments[i]; + if (cmt.value.indexOf('@hot reset') !== -1) { + hasForceReset = true; + break; + } + } + + hasForceResetCommentByFile.set(file, hasForceReset); + return hasForceReset; + } + + function createArgumentsForSignature(node, signature, scope) { const {key, customHooks} = signature; + + let forceReset = hasForceResetComment(scope.path); + let customHooksInScope = []; + customHooks.forEach(callee => { + // Check if a correponding binding exists where we emit the signature. + let bindingName; + switch (callee.type) { + case 'MemberExpression': + if (callee.object.type === 'Identifier') { + bindingName = callee.object.name; + } + break; + case 'Identifier': + bindingName = callee.name; + break; + } + if (scope.hasBinding(bindingName)) { + customHooksInScope.push(callee); + } else { + // We don't have anything to put in the array because Hook is out of scope. + // Since it could potentially have been edited, remount the component. + forceReset = true; + } + }); + const args = [node, t.stringLiteral(key)]; - if (customHooks.length > 0) { - args.push(t.arrowFunctionExpression([], t.arrayExpression(customHooks))); + if (forceReset || customHooksInScope.length > 0) { + args.push(t.booleanLiteral(forceReset)); + } + if (customHooksInScope.length > 0) { + args.push( + t.arrowFunctionExpression([], t.arrayExpression(customHooksInScope)), + ); } return args; } @@ -376,7 +430,11 @@ export default function(babel) { t.expressionStatement( t.callExpression( t.identifier('__signature__'), - createArgumentsForSignature(id, signature), + createArgumentsForSignature( + id, + signature, + insertAfterPath.scope, + ), ), ), ); @@ -418,7 +476,11 @@ export default function(babel) { t.expressionStatement( t.callExpression( t.identifier('__signature__'), - createArgumentsForSignature(path.parent.id, signature), + createArgumentsForSignature( + path.parent.id, + signature, + insertAfterPath.scope, + ), ), ), ); @@ -428,7 +490,7 @@ export default function(babel) { path.replaceWith( t.callExpression( t.identifier('__signature__'), - createArgumentsForSignature(node, signature), + createArgumentsForSignature(node, signature, path.scope), ), ); // Result: let Foo = hoc(__signature(() => {}, ...)) diff --git a/packages/react-fresh/src/ReactFreshRuntime.js b/packages/react-fresh/src/ReactFreshRuntime.js index da9bd3c4d8265..ba8ad167b0bac 100644 --- a/packages/react-fresh/src/ReactFreshRuntime.js +++ b/packages/react-fresh/src/ReactFreshRuntime.js @@ -16,6 +16,7 @@ import {REACT_MEMO_TYPE, REACT_FORWARD_REF_TYPE} from 'shared/ReactSymbols'; type Signature = {| key: string, + forceReset: boolean, getCustomHooks: () => Array, |}; @@ -45,6 +46,9 @@ function haveEqualSignatures(prevType, nextType) { if (prevSignature.key !== nextSignature.key) { return false; } + if (nextSignature.forceReset) { + return false; + } // TODO: we might need to calculate previous signature earlier in practice, // such as during the first time a component is resolved. We'll revisit this. @@ -63,6 +67,24 @@ function haveEqualSignatures(prevType, nextType) { return true; } +function isReactClass(type) { + return type.prototype && type.prototype.isReactComponent; +} + +function canPreserveStateBetween(prevType, nextType) { + if (isReactClass(prevType) || isReactClass(nextType)) { + return false; + } + if (haveEqualSignatures(prevType, nextType)) { + return true; + } + return false; +} + +function resolveFamily(type) { + return familiesByType.get(type); +} + export function prepareUpdate(): HotUpdate { const staleFamilies = new Set(); const updatedFamilies = new Set(); @@ -78,7 +100,7 @@ export function prepareUpdate(): HotUpdate { family.current = nextType; // Determine whether this should be a re-render or a re-mount. - if (haveEqualSignatures(prevType, nextType)) { + if (canPreserveStateBetween(prevType, nextType)) { updatedFamilies.add(family); } else { staleFamilies.add(family); @@ -86,7 +108,7 @@ export function prepareUpdate(): HotUpdate { }); return { - familiesByType, + resolveFamily, updatedFamilies, staleFamilies, }; @@ -135,10 +157,12 @@ export function register(type: any, id: string): void { export function setSignature( type: any, key: string, + forceReset?: boolean = false, getCustomHooks?: () => Array, ): void { allSignaturesByType.set(type, { key, + forceReset, getCustomHooks: getCustomHooks || (() => []), }); } diff --git a/packages/react-fresh/src/__tests__/ReactFresh-test.js b/packages/react-fresh/src/__tests__/ReactFresh-test.js index 4cca204c000fc..0cc1a6a544840 100644 --- a/packages/react-fresh/src/__tests__/ReactFresh-test.js +++ b/packages/react-fresh/src/__tests__/ReactFresh-test.js @@ -72,8 +72,8 @@ describe('ReactFresh', () => { ReactFreshRuntime.register(type, id); } - function __signature__(type, key, getCustomHooks) { - ReactFreshRuntime.setSignature(type, key, getCustomHooks); + function __signature__(type, key, forceReset, getCustomHooks) { + ReactFreshRuntime.setSignature(type, key, forceReset, getCustomHooks); return type; } @@ -2709,4 +2709,229 @@ describe('ReactFresh', () => { expect(helloNode.textContent).toBe('Nice.'); } }); + + it('remounts classes on every edit', () => { + if (__DEV__) { + let HelloV1 = render(() => { + class Hello extends React.Component { + state = {count: 0}; + handleClick = () => { + this.setState(prev => ({ + count: prev.count + 1, + })); + }; + render() { + return ( +

+ {this.state.count} +

+ ); + } + } + // For classes, we wouldn't do this call via Babel plugin. + // Instead, we'd do it at module boundaries. + // Normally classes would get a different type and remount anyway, + // but at module boundaries we may want to prevent propagation. + // However we still want to force a remount and use latest version. + __register__(Hello, 'Hello'); + return Hello; + }); + + // Bump the state before patching. + const el = container.firstChild; + expect(el.textContent).toBe('0'); + expect(el.style.color).toBe('blue'); + act(() => { + el.dispatchEvent(new MouseEvent('click', {bubbles: true})); + }); + expect(el.textContent).toBe('1'); + + // Perform a hot update. + let HelloV2 = patch(() => { + class Hello extends React.Component { + state = {count: 0}; + handleClick = () => { + this.setState(prev => ({ + count: prev.count + 1, + })); + }; + render() { + return ( +

+ {this.state.count} +

+ ); + } + } + __register__(Hello, 'Hello'); + return Hello; + }); + + // It should have remounted the class. + expect(container.firstChild).not.toBe(el); + const newEl = container.firstChild; + expect(newEl.textContent).toBe('0'); + expect(newEl.style.color).toBe('red'); + act(() => { + newEl.dispatchEvent(new MouseEvent('click', {bubbles: true})); + }); + expect(newEl.textContent).toBe('1'); + + // Now top-level renders of both types resolve to latest. + render(() => HelloV1); + render(() => HelloV2); + expect(container.firstChild).toBe(newEl); + expect(newEl.style.color).toBe('red'); + expect(newEl.textContent).toBe('1'); + + let HelloV3 = patch(() => { + class Hello extends React.Component { + state = {count: 0}; + handleClick = () => { + this.setState(prev => ({ + count: prev.count + 1, + })); + }; + render() { + return ( +

+ {this.state.count} +

+ ); + } + } + __register__(Hello, 'Hello'); + return Hello; + }); + + // It should have remounted the class again. + expect(container.firstChild).not.toBe(el); + const finalEl = container.firstChild; + expect(finalEl.textContent).toBe('0'); + expect(finalEl.style.color).toBe('orange'); + act(() => { + finalEl.dispatchEvent(new MouseEvent('click', {bubbles: true})); + }); + expect(finalEl.textContent).toBe('1'); + + render(() => HelloV3); + render(() => HelloV2); + render(() => HelloV1); + expect(container.firstChild).toBe(finalEl); + expect(finalEl.style.color).toBe('orange'); + expect(finalEl.textContent).toBe('1'); + } + }); + + it('remounts on conversion from class to function and back', () => { + if (__DEV__) { + let HelloV1 = render(() => { + function Hello() { + const [val, setVal] = React.useState(0); + return ( +

setVal(val + 1)}> + {val} +

+ ); + } + __register__(Hello, 'Hello'); + return Hello; + }); + + // Bump the state before patching. + const el = container.firstChild; + expect(el.textContent).toBe('0'); + expect(el.style.color).toBe('blue'); + act(() => { + el.dispatchEvent(new MouseEvent('click', {bubbles: true})); + }); + expect(el.textContent).toBe('1'); + + // Perform a hot update that turns it into a class. + let HelloV2 = patch(() => { + class Hello extends React.Component { + state = {count: 0}; + handleClick = () => { + this.setState(prev => ({ + count: prev.count + 1, + })); + }; + render() { + return ( +

+ {this.state.count} +

+ ); + } + } + __register__(Hello, 'Hello'); + return Hello; + }); + + // It should have remounted. + expect(container.firstChild).not.toBe(el); + const newEl = container.firstChild; + expect(newEl.textContent).toBe('0'); + expect(newEl.style.color).toBe('red'); + act(() => { + newEl.dispatchEvent(new MouseEvent('click', {bubbles: true})); + }); + expect(newEl.textContent).toBe('1'); + + // Now top-level renders of both types resolve to latest. + render(() => HelloV1); + render(() => HelloV2); + expect(container.firstChild).toBe(newEl); + expect(newEl.style.color).toBe('red'); + expect(newEl.textContent).toBe('1'); + + // Now convert it back to a function. + let HelloV3 = patch(() => { + function Hello() { + const [val, setVal] = React.useState(0); + return ( +

setVal(val + 1)}> + {val} +

+ ); + } + __register__(Hello, 'Hello'); + return Hello; + }); + + // It should have remounted again. + expect(container.firstChild).not.toBe(el); + const finalEl = container.firstChild; + expect(finalEl.textContent).toBe('0'); + expect(finalEl.style.color).toBe('orange'); + act(() => { + finalEl.dispatchEvent(new MouseEvent('click', {bubbles: true})); + }); + expect(finalEl.textContent).toBe('1'); + + render(() => HelloV3); + render(() => HelloV2); + render(() => HelloV1); + expect(container.firstChild).toBe(finalEl); + expect(finalEl.style.color).toBe('orange'); + expect(finalEl.textContent).toBe('1'); + + // Now that it's a function, verify edits keep state. + patch(() => { + function Hello() { + const [val, setVal] = React.useState(0); + return ( +

setVal(val + 1)}> + {val} +

+ ); + } + __register__(Hello, 'Hello'); + return Hello; + }); + expect(container.firstChild).toBe(finalEl); + expect(finalEl.style.color).toBe('purple'); + expect(finalEl.textContent).toBe('1'); + } + }); }); diff --git a/packages/react-fresh/src/__tests__/ReactFreshBabelPlugin-test.js b/packages/react-fresh/src/__tests__/ReactFreshBabelPlugin-test.js index e75faa766c57c..49ed9c2280b56 100644 --- a/packages/react-fresh/src/__tests__/ReactFreshBabelPlugin-test.js +++ b/packages/react-fresh/src/__tests__/ReactFreshBabelPlugin-test.js @@ -386,4 +386,25 @@ describe('ReactFreshBabelPlugin', () => { `), ).toMatchSnapshot(); }); + + it('generates valid signature for exotic ways to call Hooks', () => { + expect( + transform(` + import FancyHook from 'fancy'; + + export default function App() { + function useFancyState() { + const [foo, setFoo] = React.useState(0); + useFancyEffect(); + return foo; + } + const bar = useFancyState(); + const baz = FancyHook.useThing(); + React.useState(); + useThePlatform(); + return

{bar}{baz}

; + } + `), + ).toMatchSnapshot(); + }); }); diff --git a/packages/react-fresh/src/__tests__/ReactFreshIntegration-test.js b/packages/react-fresh/src/__tests__/ReactFreshIntegration-test.js index e50818e923621..97cdb246e3da9 100644 --- a/packages/react-fresh/src/__tests__/ReactFreshIntegration-test.js +++ b/packages/react-fresh/src/__tests__/ReactFreshIntegration-test.js @@ -85,8 +85,8 @@ describe('ReactFreshIntegration', () => { ReactFreshRuntime.register(type, id); } - function __signature__(type, key, getCustomHooks) { - ReactFreshRuntime.setSignature(type, key, getCustomHooks); + function __signature__(type, key, forceReset, getCustomHooks) { + ReactFreshRuntime.setSignature(type, key, forceReset, getCustomHooks); return type; } @@ -551,6 +551,49 @@ describe('ReactFreshIntegration', () => { } }); + it('does not get confused by Hooks defined inline', () => { + // This is not a recommended pattern but at least it shouldn't break. + if (__DEV__) { + render(` + const App = () => { + const useFancyState = (initialState) => { + const result = React.useState(initialState); + return result; + }; + const [x, setX] = useFancyState('X1'); + const [y, setY] = useFancyState('Y1'); + return

A{x}{y}

; + }; + + export default App; + `); + let el = container.firstChild; + expect(el.textContent).toBe('AX1Y1'); + + patch(` + const App = () => { + const useFancyState = (initialState) => { + const result = React.useState(initialState); + return result; + }; + const [x, setX] = useFancyState('X2'); + const [y, setY] = useFancyState('Y2'); + return

B{x}{y}

; + }; + + export default App; + `); + // Remount even though nothing changed because + // the custom Hook is inside -- and so we don't + // really know whether its signature has changed. + // We could potentially make it work, but for now + // let's assert we don't crash with confusing errors. + expect(container.firstChild).not.toBe(el); + el = container.firstChild; + expect(el.textContent).toBe('BX2Y2'); + } + }); + it('remounts component if custom hook it uses changes order', () => { if (__DEV__) { render(` @@ -691,4 +734,103 @@ describe('ReactFreshIntegration', () => { expect(container.textContent).toBe('Parent Child useMyThing'); } }); + + it('resets state on every edit with @hot reset annotation', () => { + if (__DEV__) { + render(` + const {useState} = React; + + export default function App() { + const [foo, setFoo] = useState(1); + return

A{foo}

; + } + `); + let el = container.firstChild; + expect(el.textContent).toBe('A1'); + + patch(` + const {useState} = React; + + export default function App() { + const [foo, setFoo] = useState('ignored'); + return

B{foo}

; + } + `); + // Same state variable name, so state is preserved. + expect(container.firstChild).toBe(el); + expect(el.textContent).toBe('B1'); + + patch(` + const {useState} = React; + + /* @hot reset */ + + export default function App() { + const [bar, setBar] = useState(2); + return

C{bar}

; + } + `); + // Found remount annotation, so state is reset. + expect(container.firstChild).not.toBe(el); + el = container.firstChild; + expect(el.textContent).toBe('C2'); + + patch(` + const {useState} = React; + + export default function App() { + + // @hot reset + + const [bar, setBar] = useState(3); + return

D{bar}

; + } + `); + // Found remount annotation, so state is reset. + expect(container.firstChild).not.toBe(el); + el = container.firstChild; + expect(el.textContent).toBe('D3'); + + patch(` + const {useState} = React; + + export default function App() { + const [bar, setBar] = useState(4); + return

E{bar}

; + } + `); + // There is no remount annotation anymore, + // so preserve the previous state. + expect(container.firstChild).toBe(el); + expect(el.textContent).toBe('E3'); + + patch(` + const {useState} = React; + + export default function App() { + const [bar, setBar] = useState(4); + return

F{bar}

; + } + `); + // Continue editing. + expect(container.firstChild).toBe(el); + expect(el.textContent).toBe('F3'); + + patch(` + const {useState} = React; + + export default function App() { + + /* @hot reset */ + + const [bar, setBar] = useState(5); + return

G{bar}

; + } + `); + // Force remount one last time. + expect(container.firstChild).not.toBe(el); + el = container.firstChild; + expect(el.textContent).toBe('G5'); + } + }); }); diff --git a/packages/react-fresh/src/__tests__/__snapshots__/ReactFreshBabelPlugin-test.js.snap b/packages/react-fresh/src/__tests__/__snapshots__/ReactFreshBabelPlugin-test.js.snap index fba2c9cff1d72..87aa1c73c30d3 100644 --- a/packages/react-fresh/src/__tests__/__snapshots__/ReactFreshBabelPlugin-test.js.snap +++ b/packages/react-fresh/src/__tests__/__snapshots__/ReactFreshBabelPlugin-test.js.snap @@ -56,6 +56,35 @@ __register__(_c5, "B$React.memo"); __register__(_c6, "B"); `; +exports[`ReactFreshBabelPlugin generates valid signature for exotic ways to call Hooks 1`] = ` + +import FancyHook from 'fancy'; + +export default function App() { + function useFancyState() { + const [foo, setFoo] = React.useState(0); + useFancyEffect(); + return foo; + } + + __signature__(useFancyState, 'useState{[foo, setFoo]}\\nuseFancyEffect{}', true); + + const bar = useFancyState(); + const baz = FancyHook.useThing(); + React.useState(); + useThePlatform(); + return

{bar}{baz}

; +} + +__signature__(App, 'useFancyState{bar}\\nuseThing{baz}\\nuseState{}\\nuseThePlatform{}', true, () => [FancyHook.useThing]); + +_c = App; + +var _c; + +__register__(_c, 'App'); +`; + exports[`ReactFreshBabelPlugin ignores HOC definitions 1`] = ` let connect = () => { @@ -115,7 +144,7 @@ function useFancyState() { return foo; } -__signature__(useFancyState, "useState{[foo, setFoo]}\\nuseFancyEffect{}", () => [useFancyEffect]); +__signature__(useFancyState, "useState{[foo, setFoo]}\\nuseFancyEffect{}", false, () => [useFancyEffect]); const useFancyEffect = () => { React.useEffect(() => {}); @@ -128,7 +157,7 @@ export default function App() { return

{bar}

; } -__signature__(App, "useFancyState{bar}", () => [useFancyState]); +__signature__(App, "useFancyState{bar}", false, () => [useFancyState]); _c = App; diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 589c482bc1178..e3e7ffbf6d1b8 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -54,6 +54,7 @@ import getComponentName from 'shared/getComponentName'; import {isDevToolsPresent} from './ReactFiberDevToolsHook'; import { + resolveClassForHotReloading, resolveFunctionForHotReloading, resolveForwardRefForHotReloading, } from './ReactFiberHotReloading'; @@ -449,6 +450,9 @@ export function createWorkInProgress( case SimpleMemoComponent: workInProgress.type = resolveFunctionForHotReloading(current.type); break; + case ClassComponent: + workInProgress.type = resolveClassForHotReloading(current.type); + break; case ForwardRef: workInProgress.type = resolveForwardRefForHotReloading(current.type); break; @@ -496,6 +500,9 @@ export function createFiberFromTypeAndProps( if (typeof type === 'function') { if (shouldConstruct(type)) { fiberTag = ClassComponent; + if (__DEV__) { + resolvedType = resolveClassForHotReloading(resolvedType); + } } else { if (__DEV__) { resolvedType = resolveFunctionForHotReloading(resolvedType); diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 6e0d59f001343..600389dc32b40 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -71,7 +71,11 @@ import { getCurrentFiberStackInDev, } from './ReactCurrentFiber'; import {startWorkTimer, cancelWorkTimer} from './ReactDebugFiberPerf'; -import {resolveFunctionForHotReloading} from './ReactFiberHotReloading'; +import { + resolveFunctionForHotReloading, + resolveForwardRefForHotReloading, + resolveClassForHotReloading, +} from './ReactFiberHotReloading'; import { mountChildFibers, @@ -1054,6 +1058,11 @@ function mountLazyComponent( break; } case ClassComponent: { + if (__DEV__) { + workInProgress.type = Component = resolveClassForHotReloading( + Component, + ); + } child = updateClassComponent( null, workInProgress, @@ -1065,7 +1074,7 @@ function mountLazyComponent( } case ForwardRef: { if (__DEV__) { - workInProgress.type = Component = resolveFunctionForHotReloading( + workInProgress.type = Component = resolveForwardRefForHotReloading( Component, ); } diff --git a/packages/react-reconciler/src/ReactFiberHotReloading.js b/packages/react-reconciler/src/ReactFiberHotReloading.js index 56b6365d3d7b3..60f625bd5112a 100644 --- a/packages/react-reconciler/src/ReactFiberHotReloading.js +++ b/packages/react-reconciler/src/ReactFiberHotReloading.js @@ -18,6 +18,7 @@ import { } from './ReactFiberWorkLoop'; import {Sync} from './ReactFiberExpirationTime'; import { + ClassComponent, FunctionComponent, ForwardRef, MemoComponent, @@ -34,22 +35,22 @@ export type Family = {| |}; export type HotUpdate = {| - familiesByType: WeakMap, + resolveFamily: (any => Family | void) | null, staleFamilies: Set, updatedFamilies: Set, |}; -let familiesByType: WeakMap | null = null; +let resolveFamily: (any => Family | void) | null = null; // $FlowFixMe Flow gets confused by a WeakSet feature check below. let failedBoundaries: WeakSet | null = null; export function resolveFunctionForHotReloading(type: any): any { if (__DEV__) { - if (familiesByType === null) { + if (resolveFamily === null) { // Hot reloading is disabled. return type; } - let family = familiesByType.get(type); + let family = resolveFamily(type); if (family === undefined) { return type; } @@ -60,13 +61,18 @@ export function resolveFunctionForHotReloading(type: any): any { } } +export function resolveClassForHotReloading(type: any): any { + // No implementation differences. + return resolveFunctionForHotReloading(type); +} + export function resolveForwardRefForHotReloading(type: any): any { if (__DEV__) { - if (familiesByType === null) { + if (resolveFamily === null) { // Hot reloading is disabled. return type; } - let family = familiesByType.get(type); + let family = resolveFamily(type); if (family === undefined) { // Check if we're dealing with a real forwardRef. Don't want to crash early. if ( @@ -103,7 +109,7 @@ export function isCompatibleFamilyForHotReloading( element: ReactElement, ): boolean { if (__DEV__) { - if (familiesByType === null) { + if (resolveFamily === null) { // Hot reloading is disabled. return false; } @@ -120,6 +126,12 @@ export function isCompatibleFamilyForHotReloading( : null; switch (fiber.tag) { + case ClassComponent: { + if (typeof nextType === 'function') { + needsCompareFamilies = true; + } + break; + } case FunctionComponent: { if (typeof nextType === 'function') { needsCompareFamilies = true; @@ -162,11 +174,8 @@ export function isCompatibleFamilyForHotReloading( // 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 = familiesByType.get(prevType); - if ( - prevFamily !== undefined && - prevFamily === familiesByType.get(nextType) - ) { + const prevFamily = resolveFamily(prevType); + if (prevFamily !== undefined && prevFamily === resolveFamily(nextType)) { return true; } } @@ -178,7 +187,7 @@ export function isCompatibleFamilyForHotReloading( export function markFailedErrorBoundaryForHotReloading(fiber: Fiber) { if (__DEV__) { - if (familiesByType === null) { + if (resolveFamily === null) { // Not hot reloading. return; } @@ -195,7 +204,7 @@ export function markFailedErrorBoundaryForHotReloading(fiber: Fiber) { export function scheduleHotUpdate(root: FiberRoot, hotUpdate: HotUpdate): void { if (__DEV__) { // TODO: warn if its identity changes over time? - familiesByType = hotUpdate.familiesByType; + resolveFamily = hotUpdate.resolveFamily; const {staleFamilies, updatedFamilies} = hotUpdate; flushPassiveEffects(); @@ -221,6 +230,7 @@ function scheduleFibersWithFamiliesRecursively( switch (tag) { case FunctionComponent: case SimpleMemoComponent: + case ClassComponent: candidateType = type; break; case ForwardRef: @@ -230,14 +240,14 @@ function scheduleFibersWithFamiliesRecursively( break; } - if (familiesByType === null) { - throw new Error('Expected familiesByType to be set during hot reload.'); + 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 = familiesByType.get(candidateType); + const family = resolveFamily(candidateType); if (family !== undefined) { if (staleFamilies.has(family)) { needsRemount = true;