@samitouri / QOS-React-2 / commits / d0e041aee8

[Fresh] Support classes by force-remounting them on edit (#15801)

* Remount classes during hot reload * Fix a crash when Hook isn't in scope inside the signature * Minor tweaks * Support a comment annotation to force state reset * Refactoring: pass a function instead of WeakMap This hides the implementation a little bit and reduces how much React knows about the underlying mechanism. * Refactor: use forceReset to remount unknown Hooks We already have the logic to reset a component, so let's just reuse it instead of that special case.

Dan Abramov committed Jun 5, 2019 at 16:13 UTC d0e041aee83b50e54176d2dc957d86c0f571257e
9 files changed +562 -33
packages/react-fresh/src/ReactFreshBabelPlugin.js
+68 -6
@@ -223,11 +223,65 @@ export default function(babel) {
223 };
224 }
225
226 - function createArgumentsForSignature(node, signature) {
226 + let hasForceResetCommentByFile = new WeakMap();
227 +
228 + // We let user do /* @hot reset */ to reset state in the whole file.
229 + function hasForceResetComment(path) {
230 + const file = path.hub.file;
231 + let hasForceReset = hasForceResetCommentByFile.get(file);
232 + if (hasForceReset !== undefined) {
233 + return hasForceReset;
234 + }
235 +
236 + hasForceReset = false;
237 + const comments = file.ast.comments;
238 + for (let i = 0; i < comments.length; i++) {
239 + const cmt = comments[i];
240 + if (cmt.value.indexOf('@hot reset') !== -1) {
241 + hasForceReset = true;
242 + break;
243 + }
244 + }
245 +
246 + hasForceResetCommentByFile.set(file, hasForceReset);
247 + return hasForceReset;
248 + }
249 +
250 + function createArgumentsForSignature(node, signature, scope) {
251 const {key, customHooks} = signature;
252 +
253 + let forceReset = hasForceResetComment(scope.path);
254 + let customHooksInScope = [];
255 + customHooks.forEach(callee => {
256 + // Check if a correponding binding exists where we emit the signature.
257 + let bindingName;
258 + switch (callee.type) {
259 + case 'MemberExpression':
260 + if (callee.object.type === 'Identifier') {
261 + bindingName = callee.object.name;
262 + }
263 + break;
264 + case 'Identifier':
265 + bindingName = callee.name;
266 + break;
267 + }
268 + if (scope.hasBinding(bindingName)) {
269 + customHooksInScope.push(callee);
270 + } else {
271 + // We don't have anything to put in the array because Hook is out of scope.
272 + // Since it could potentially have been edited, remount the component.
273 + forceReset = true;
274 + }
275 + });
276 +
277 const args = [node, t.stringLiteral(key)];
229 - if (customHooks.length > 0) {
230 - args.push(t.arrowFunctionExpression([], t.arrayExpression(customHooks)));
278 + if (forceReset || customHooksInScope.length > 0) {
279 + args.push(t.booleanLiteral(forceReset));
280 + }
281 + if (customHooksInScope.length > 0) {
282 + args.push(
283 + t.arrowFunctionExpression([], t.arrayExpression(customHooksInScope)),
284 + );
285 }
286 return args;
287 }
@@ -376,7 +430,11 @@ export default function(babel) {
430 t.expressionStatement(
431 t.callExpression(
432 t.identifier('__signature__'),
379 - createArgumentsForSignature(id, signature),
433 + createArgumentsForSignature(
434 + id,
435 + signature,
436 + insertAfterPath.scope,
437 + ),
438 ),
439 ),
440 );
@@ -418,7 +476,11 @@ export default function(babel) {
476 t.expressionStatement(
477 t.callExpression(
478 t.identifier('__signature__'),
421 - createArgumentsForSignature(path.parent.id, signature),
479 + createArgumentsForSignature(
480 + path.parent.id,
481 + signature,
482 + insertAfterPath.scope,
483 + ),
484 ),
485 ),
486 );
@@ -428,7 +490,7 @@ export default function(babel) {
490 path.replaceWith(
491 t.callExpression(
492 t.identifier('__signature__'),
431 - createArgumentsForSignature(node, signature),
493 + createArgumentsForSignature(node, signature, path.scope),
494 ),
495 );
496 // Result: let Foo = hoc(__signature(() => {}, ...))
packages/react-fresh/src/ReactFreshRuntime.js
+26 -2
@@ -16,6 +16,7 @@ import {REACT_MEMO_TYPE, REACT_FORWARD_REF_TYPE} from 'shared/ReactSymbols';
16
17 type Signature = {|
18 key: string,
19 + forceReset: boolean,
20 getCustomHooks: () => Array<Function>,
21 |};
22
@@ -45,6 +46,9 @@ function haveEqualSignatures(prevType, nextType) {
46 if (prevSignature.key !== nextSignature.key) {
47 return false;
48 }
49 + if (nextSignature.forceReset) {
50 + return false;
51 + }
52
53 // TODO: we might need to calculate previous signature earlier in practice,
54 // such as during the first time a component is resolved. We'll revisit this.
@@ -63,6 +67,24 @@ function haveEqualSignatures(prevType, nextType) {
67 return true;
68 }
69
70 +function isReactClass(type) {
71 + return type.prototype && type.prototype.isReactComponent;
72 +}
73 +
74 +function canPreserveStateBetween(prevType, nextType) {
75 + if (isReactClass(prevType) || isReactClass(nextType)) {
76 + return false;
77 + }
78 + if (haveEqualSignatures(prevType, nextType)) {
79 + return true;
80 + }
81 + return false;
82 +}
83 +
84 +function resolveFamily(type) {
85 + return familiesByType.get(type);
86 +}
87 +
88 export function prepareUpdate(): HotUpdate {
89 const staleFamilies = new Set();
90 const updatedFamilies = new Set();
@@ -78,7 +100,7 @@ export function prepareUpdate(): HotUpdate {
100 family.current = nextType;
101
102 // Determine whether this should be a re-render or a re-mount.
81 - if (haveEqualSignatures(prevType, nextType)) {
103 + if (canPreserveStateBetween(prevType, nextType)) {
104 updatedFamilies.add(family);
105 } else {
106 staleFamilies.add(family);
@@ -86,7 +108,7 @@ export function prepareUpdate(): HotUpdate {
108 });
109
110 return {
89 - familiesByType,
111 + resolveFamily,
112 updatedFamilies,
113 staleFamilies,
114 };
@@ -135,10 +157,12 @@ export function register(type: any, id: string): void {
157 export function setSignature(
158 type: any,
159 key: string,
160 + forceReset?: boolean = false,
161 getCustomHooks?: () => Array<Function>,
162 ): void {
163 allSignaturesByType.set(type, {
164 key,
165 + forceReset,
166 getCustomHooks: getCustomHooks || (() => []),
167 });
168 }
packages/react-fresh/src/__tests__/ReactFresh-test.js
+227 -2
@@ -72,8 +72,8 @@ describe('ReactFresh', () => {
72 ReactFreshRuntime.register(type, id);
73 }
74
75 - function __signature__(type, key, getCustomHooks) {
76 - ReactFreshRuntime.setSignature(type, key, getCustomHooks);
75 + function __signature__(type, key, forceReset, getCustomHooks) {
76 + ReactFreshRuntime.setSignature(type, key, forceReset, getCustomHooks);
77 return type;
78 }
79
@@ -2709,4 +2709,229 @@ describe('ReactFresh', () => {
2709 expect(helloNode.textContent).toBe('Nice.');
2710 }
2711 });
2712 +
2713 + it('remounts classes on every edit', () => {
2714 + if (__DEV__) {
2715 + let HelloV1 = render(() => {
2716 + class Hello extends React.Component {
2717 + state = {count: 0};
2718 + handleClick = () => {
2719 + this.setState(prev => ({
2720 + count: prev.count + 1,
2721 + }));
2722 + };
2723 + render() {
2724 + return (
2725 + <p style={{color: 'blue'}} onClick={this.handleClick}>
2726 + {this.state.count}
2727 + </p>
2728 + );
2729 + }
2730 + }
2731 + // For classes, we wouldn't do this call via Babel plugin.
2732 + // Instead, we'd do it at module boundaries.
2733 + // Normally classes would get a different type and remount anyway,
2734 + // but at module boundaries we may want to prevent propagation.
2735 + // However we still want to force a remount and use latest version.
2736 + __register__(Hello, 'Hello');
2737 + return Hello;
2738 + });
2739 +
2740 + // Bump the state before patching.
2741 + const el = container.firstChild;
2742 + expect(el.textContent).toBe('0');
2743 + expect(el.style.color).toBe('blue');
2744 + act(() => {
2745 + el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
2746 + });
2747 + expect(el.textContent).toBe('1');
2748 +
2749 + // Perform a hot update.
2750 + let HelloV2 = patch(() => {
2751 + class Hello extends React.Component {
2752 + state = {count: 0};
2753 + handleClick = () => {
2754 + this.setState(prev => ({
2755 + count: prev.count + 1,
2756 + }));
2757 + };
2758 + render() {
2759 + return (
2760 + <p style={{color: 'red'}} onClick={this.handleClick}>
2761 + {this.state.count}
2762 + </p>
2763 + );
2764 + }
2765 + }
2766 + __register__(Hello, 'Hello');
2767 + return Hello;
2768 + });
2769 +
2770 + // It should have remounted the class.
2771 + expect(container.firstChild).not.toBe(el);
2772 + const newEl = container.firstChild;
2773 + expect(newEl.textContent).toBe('0');
2774 + expect(newEl.style.color).toBe('red');
2775 + act(() => {
2776 + newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
2777 + });
2778 + expect(newEl.textContent).toBe('1');
2779 +
2780 + // Now top-level renders of both types resolve to latest.
2781 + render(() => HelloV1);
2782 + render(() => HelloV2);
2783 + expect(container.firstChild).toBe(newEl);
2784 + expect(newEl.style.color).toBe('red');
2785 + expect(newEl.textContent).toBe('1');
2786 +
2787 + let HelloV3 = patch(() => {
2788 + class Hello extends React.Component {
2789 + state = {count: 0};
2790 + handleClick = () => {
2791 + this.setState(prev => ({
2792 + count: prev.count + 1,
2793 + }));
2794 + };
2795 + render() {
2796 + return (
2797 + <p style={{color: 'orange'}} onClick={this.handleClick}>
2798 + {this.state.count}
2799 + </p>
2800 + );
2801 + }
2802 + }
2803 + __register__(Hello, 'Hello');
2804 + return Hello;
2805 + });
2806 +
2807 + // It should have remounted the class again.
2808 + expect(container.firstChild).not.toBe(el);
2809 + const finalEl = container.firstChild;
2810 + expect(finalEl.textContent).toBe('0');
2811 + expect(finalEl.style.color).toBe('orange');
2812 + act(() => {
2813 + finalEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
2814 + });
2815 + expect(finalEl.textContent).toBe('1');
2816 +
2817 + render(() => HelloV3);
2818 + render(() => HelloV2);
2819 + render(() => HelloV1);
2820 + expect(container.firstChild).toBe(finalEl);
2821 + expect(finalEl.style.color).toBe('orange');
2822 + expect(finalEl.textContent).toBe('1');
2823 + }
2824 + });
2825 +
2826 + it('remounts on conversion from class to function and back', () => {
2827 + if (__DEV__) {
2828 + let HelloV1 = render(() => {
2829 + function Hello() {
2830 + const [val, setVal] = React.useState(0);
2831 + return (
2832 + <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
2833 + {val}
2834 + </p>
2835 + );
2836 + }
2837 + __register__(Hello, 'Hello');
2838 + return Hello;
2839 + });
2840 +
2841 + // Bump the state before patching.
2842 + const el = container.firstChild;
2843 + expect(el.textContent).toBe('0');
2844 + expect(el.style.color).toBe('blue');
2845 + act(() => {
2846 + el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
2847 + });
2848 + expect(el.textContent).toBe('1');
2849 +
2850 + // Perform a hot update that turns it into a class.
2851 + let HelloV2 = patch(() => {
2852 + class Hello extends React.Component {
2853 + state = {count: 0};
2854 + handleClick = () => {
2855 + this.setState(prev => ({
2856 + count: prev.count + 1,
2857 + }));
2858 + };
2859 + render() {
2860 + return (
2861 + <p style={{color: 'red'}} onClick={this.handleClick}>
2862 + {this.state.count}
2863 + </p>
2864 + );
2865 + }
2866 + }
2867 + __register__(Hello, 'Hello');
2868 + return Hello;
2869 + });
2870 +
2871 + // It should have remounted.
2872 + expect(container.firstChild).not.toBe(el);
2873 + const newEl = container.firstChild;
2874 + expect(newEl.textContent).toBe('0');
2875 + expect(newEl.style.color).toBe('red');
2876 + act(() => {
2877 + newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
2878 + });
2879 + expect(newEl.textContent).toBe('1');
2880 +
2881 + // Now top-level renders of both types resolve to latest.
2882 + render(() => HelloV1);
2883 + render(() => HelloV2);
2884 + expect(container.firstChild).toBe(newEl);
2885 + expect(newEl.style.color).toBe('red');
2886 + expect(newEl.textContent).toBe('1');
2887 +
2888 + // Now convert it back to a function.
2889 + let HelloV3 = patch(() => {
2890 + function Hello() {
2891 + const [val, setVal] = React.useState(0);
2892 + return (
2893 + <p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
2894 + {val}
2895 + </p>
2896 + );
2897 + }
2898 + __register__(Hello, 'Hello');
2899 + return Hello;
2900 + });
2901 +
2902 + // It should have remounted again.
2903 + expect(container.firstChild).not.toBe(el);
2904 + const finalEl = container.firstChild;
2905 + expect(finalEl.textContent).toBe('0');
2906 + expect(finalEl.style.color).toBe('orange');
2907 + act(() => {
2908 + finalEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
2909 + });
2910 + expect(finalEl.textContent).toBe('1');
2911 +
2912 + render(() => HelloV3);
2913 + render(() => HelloV2);
2914 + render(() => HelloV1);
2915 + expect(container.firstChild).toBe(finalEl);
2916 + expect(finalEl.style.color).toBe('orange');
2917 + expect(finalEl.textContent).toBe('1');
2918 +
2919 + // Now that it's a function, verify edits keep state.
2920 + patch(() => {
2921 + function Hello() {
2922 + const [val, setVal] = React.useState(0);
2923 + return (
2924 + <p style={{color: 'purple'}} onClick={() => setVal(val + 1)}>
2925 + {val}
2926 + </p>
2927 + );
2928 + }
2929 + __register__(Hello, 'Hello');
2930 + return Hello;
2931 + });
2932 + expect(container.firstChild).toBe(finalEl);
2933 + expect(finalEl.style.color).toBe('purple');
2934 + expect(finalEl.textContent).toBe('1');
2935 + }
2936 + });
2937 });
packages/react-fresh/src/__tests__/ReactFreshBabelPlugin-test.js
+21
@@ -386,4 +386,25 @@ describe('ReactFreshBabelPlugin', () => {
386 `),
387 ).toMatchSnapshot();
388 });
389 +
390 + it('generates valid signature for exotic ways to call Hooks', () => {
391 + expect(
392 + transform(`
393 + import FancyHook from 'fancy';
394 +
395 + export default function App() {
396 + function useFancyState() {
397 + const [foo, setFoo] = React.useState(0);
398 + useFancyEffect();
399 + return foo;
400 + }
401 + const bar = useFancyState();
402 + const baz = FancyHook.useThing();
403 + React.useState();
404 + useThePlatform();
405 + return <h1>{bar}{baz}</h1>;
406 + }
407 + `),
408 + ).toMatchSnapshot();
409 + });
410 });
packages/react-fresh/src/__tests__/ReactFreshIntegration-test.js
+144 -2
@@ -85,8 +85,8 @@ describe('ReactFreshIntegration', () => {
85 ReactFreshRuntime.register(type, id);
86 }
87
88 - function __signature__(type, key, getCustomHooks) {
89 - ReactFreshRuntime.setSignature(type, key, getCustomHooks);
88 + function __signature__(type, key, forceReset, getCustomHooks) {
89 + ReactFreshRuntime.setSignature(type, key, forceReset, getCustomHooks);
90 return type;
91 }
92
@@ -551,6 +551,49 @@ describe('ReactFreshIntegration', () => {
551 }
552 });
553
554 + it('does not get confused by Hooks defined inline', () => {
555 + // This is not a recommended pattern but at least it shouldn't break.
556 + if (__DEV__) {
557 + render(`
558 + const App = () => {
559 + const useFancyState = (initialState) => {
560 + const result = React.useState(initialState);
561 + return result;
562 + };
563 + const [x, setX] = useFancyState('X1');
564 + const [y, setY] = useFancyState('Y1');
565 + return <h1>A{x}{y}</h1>;
566 + };
567 +
568 + export default App;
569 + `);
570 + let el = container.firstChild;
571 + expect(el.textContent).toBe('AX1Y1');
572 +
573 + patch(`
574 + const App = () => {
575 + const useFancyState = (initialState) => {
576 + const result = React.useState(initialState);
577 + return result;
578 + };
579 + const [x, setX] = useFancyState('X2');
580 + const [y, setY] = useFancyState('Y2');
581 + return <h1>B{x}{y}</h1>;
582 + };
583 +
584 + export default App;
585 + `);
586 + // Remount even though nothing changed because
587 + // the custom Hook is inside -- and so we don't
588 + // really know whether its signature has changed.
589 + // We could potentially make it work, but for now
590 + // let's assert we don't crash with confusing errors.
591 + expect(container.firstChild).not.toBe(el);
592 + el = container.firstChild;
593 + expect(el.textContent).toBe('BX2Y2');
594 + }
595 + });
596 +
597 it('remounts component if custom hook it uses changes order', () => {
598 if (__DEV__) {
599 render(`
@@ -691,4 +734,103 @@ describe('ReactFreshIntegration', () => {
734 expect(container.textContent).toBe('Parent Child useMyThing');
735 }
736 });
737 +
738 + it('resets state on every edit with @hot reset annotation', () => {
739 + if (__DEV__) {
740 + render(`
741 + const {useState} = React;
742 +
743 + export default function App() {
744 + const [foo, setFoo] = useState(1);
745 + return <h1>A{foo}</h1>;
746 + }
747 + `);
748 + let el = container.firstChild;
749 + expect(el.textContent).toBe('A1');
750 +
751 + patch(`
752 + const {useState} = React;
753 +
754 + export default function App() {
755 + const [foo, setFoo] = useState('ignored');
756 + return <h1>B{foo}</h1>;
757 + }
758 + `);
759 + // Same state variable name, so state is preserved.
760 + expect(container.firstChild).toBe(el);
761 + expect(el.textContent).toBe('B1');
762 +
763 + patch(`
764 + const {useState} = React;
765 +
766 + /* @hot reset */
767 +
768 + export default function App() {
769 + const [bar, setBar] = useState(2);
770 + return <h1>C{bar}</h1>;
771 + }
772 + `);
773 + // Found remount annotation, so state is reset.
774 + expect(container.firstChild).not.toBe(el);
775 + el = container.firstChild;
776 + expect(el.textContent).toBe('C2');
777 +
778 + patch(`
779 + const {useState} = React;
780 +
781 + export default function App() {
782 +
783 + // @hot reset
784 +
785 + const [bar, setBar] = useState(3);
786 + return <h1>D{bar}</h1>;
787 + }
788 + `);
789 + // Found remount annotation, so state is reset.
790 + expect(container.firstChild).not.toBe(el);
791 + el = container.firstChild;
792 + expect(el.textContent).toBe('D3');
793 +
794 + patch(`
795 + const {useState} = React;
796 +
797 + export default function App() {
798 + const [bar, setBar] = useState(4);
799 + return <h1>E{bar}</h1>;
800 + }
801 + `);
802 + // There is no remount annotation anymore,
803 + // so preserve the previous state.
804 + expect(container.firstChild).toBe(el);
805 + expect(el.textContent).toBe('E3');
806 +
807 + patch(`
808 + const {useState} = React;
809 +
810 + export default function App() {
811 + const [bar, setBar] = useState(4);
812 + return <h1>F{bar}</h1>;
813 + }
814 + `);
815 + // Continue editing.
816 + expect(container.firstChild).toBe(el);
817 + expect(el.textContent).toBe('F3');
818 +
819 + patch(`
820 + const {useState} = React;
821 +
822 + export default function App() {
823 +
824 + /* @hot reset */
825 +
826 + const [bar, setBar] = useState(5);
827 + return <h1>G{bar}</h1>;
828 + }
829 + `);
830 + // Force remount one last time.
831 + expect(container.firstChild).not.toBe(el);
832 + el = container.firstChild;
833 + expect(el.textContent).toBe('G5');
834 + }
835 + });
836 });
packages/react-fresh/src/__tests__/__snapshots__/ReactFreshBabelPlugin-test.js.snap
+31 -2
@@ -56,6 +56,35 @@ __register__(_c5, "B$React.memo");
56 __register__(_c6, "B");
57 `;
58
59 +exports[`ReactFreshBabelPlugin generates valid signature for exotic ways to call Hooks 1`] = `
60 +
61 +import FancyHook from 'fancy';
62 +
63 +export default function App() {
64 + function useFancyState() {
65 + const [foo, setFoo] = React.useState(0);
66 + useFancyEffect();
67 + return foo;
68 + }
69 +
70 + __signature__(useFancyState, 'useState{[foo, setFoo]}\\nuseFancyEffect{}', true);
71 +
72 + const bar = useFancyState();
73 + const baz = FancyHook.useThing();
74 + React.useState();
75 + useThePlatform();
76 + return <h1>{bar}{baz}</h1>;
77 +}
78 +
79 +__signature__(App, 'useFancyState{bar}\\nuseThing{baz}\\nuseState{}\\nuseThePlatform{}', true, () => [FancyHook.useThing]);
80 +
81 +_c = App;
82 +
83 +var _c;
84 +
85 +__register__(_c, 'App');
86 +`;
87 +
88 exports[`ReactFreshBabelPlugin ignores HOC definitions 1`] = `
89
90 let connect = () => {
@@ -115,7 +144,7 @@ function useFancyState() {
144 return foo;
145 }
146
118 -__signature__(useFancyState, "useState{[foo, setFoo]}\\nuseFancyEffect{}", () => [useFancyEffect]);
147 +__signature__(useFancyState, "useState{[foo, setFoo]}\\nuseFancyEffect{}", false, () => [useFancyEffect]);
148
149 const useFancyEffect = () => {
150 React.useEffect(() => {});
@@ -128,7 +157,7 @@ export default function App() {
157 return <h1>{bar}</h1>;
158 }
159
131 -__signature__(App, "useFancyState{bar}", () => [useFancyState]);
160 +__signature__(App, "useFancyState{bar}", false, () => [useFancyState]);
161
162 _c = App;
163
packages/react-reconciler/src/ReactFiber.js
+7
@@ -54,6 +54,7 @@ import getComponentName from 'shared/getComponentName';
54
55 import {isDevToolsPresent} from './ReactFiberDevToolsHook';
56 import {
57 + resolveClassForHotReloading,
58 resolveFunctionForHotReloading,
59 resolveForwardRefForHotReloading,
60 } from './ReactFiberHotReloading';
@@ -449,6 +450,9 @@ export function createWorkInProgress(
450 case SimpleMemoComponent:
451 workInProgress.type = resolveFunctionForHotReloading(current.type);
452 break;
453 + case ClassComponent:
454 + workInProgress.type = resolveClassForHotReloading(current.type);
455 + break;
456 case ForwardRef:
457 workInProgress.type = resolveForwardRefForHotReloading(current.type);
458 break;
@@ -496,6 +500,9 @@ export function createFiberFromTypeAndProps(
500 if (typeof type === 'function') {
501 if (shouldConstruct(type)) {
502 fiberTag = ClassComponent;
503 + if (__DEV__) {
504 + resolvedType = resolveClassForHotReloading(resolvedType);
505 + }
506 } else {
507 if (__DEV__) {
508 resolvedType = resolveFunctionForHotReloading(resolvedType);
packages/react-reconciler/src/ReactFiberBeginWork.js
+11 -2
@@ -71,7 +71,11 @@ import {
71 getCurrentFiberStackInDev,
72 } from './ReactCurrentFiber';
73 import {startWorkTimer, cancelWorkTimer} from './ReactDebugFiberPerf';
74 -import {resolveFunctionForHotReloading} from './ReactFiberHotReloading';
74 +import {
75 + resolveFunctionForHotReloading,
76 + resolveForwardRefForHotReloading,
77 + resolveClassForHotReloading,
78 +} from './ReactFiberHotReloading';
79
80 import {
81 mountChildFibers,
@@ -1054,6 +1058,11 @@ function mountLazyComponent(
1058 break;
1059 }
1060 case ClassComponent: {
1061 + if (__DEV__) {
1062 + workInProgress.type = Component = resolveClassForHotReloading(
1063 + Component,
1064 + );
1065 + }
1066 child = updateClassComponent(
1067 null,
1068 workInProgress,
@@ -1065,7 +1074,7 @@ function mountLazyComponent(
1074 }
1075 case ForwardRef: {
1076 if (__DEV__) {
1068 - workInProgress.type = Component = resolveFunctionForHotReloading(
1077 + workInProgress.type = Component = resolveForwardRefForHotReloading(
1078 Component,
1079 );
1080 }
packages/react-reconciler/src/ReactFiberHotReloading.js
+27 -17
@@ -18,6 +18,7 @@ import {
18 } from './ReactFiberWorkLoop';
19 import {Sync} from './ReactFiberExpirationTime';
20 import {
21 + ClassComponent,
22 FunctionComponent,
23 ForwardRef,
24 MemoComponent,
@@ -34,22 +35,22 @@ export type Family = {|
35 |};
36
37 export type HotUpdate = {|
37 - familiesByType: WeakMap<any, Family>,
38 + resolveFamily: (any => Family | void) | null,
39 staleFamilies: Set<Family>,
40 updatedFamilies: Set<Family>,
41 |};
42
42 -let familiesByType: WeakMap<any, Family> | null = null;
43 +let resolveFamily: (any => Family | void) | null = null;
44 // $FlowFixMe Flow gets confused by a WeakSet feature check below.
45 let failedBoundaries: WeakSet<Fiber> | null = null;
46
47 export function resolveFunctionForHotReloading(type: any): any {
48 if (__DEV__) {
48 - if (familiesByType === null) {
49 + if (resolveFamily === null) {
50 // Hot reloading is disabled.
51 return type;
52 }
52 - let family = familiesByType.get(type);
53 + let family = resolveFamily(type);
54 if (family === undefined) {
55 return type;
56 }
@@ -60,13 +61,18 @@ export function resolveFunctionForHotReloading(type: any): any {
61 }
62 }
63
64 +export function resolveClassForHotReloading(type: any): any {
65 + // No implementation differences.
66 + return resolveFunctionForHotReloading(type);
67 +}
68 +
69 export function resolveForwardRefForHotReloading(type: any): any {
70 if (__DEV__) {
65 - if (familiesByType === null) {
71 + if (resolveFamily === null) {
72 // Hot reloading is disabled.
73 return type;
74 }
69 - let family = familiesByType.get(type);
75 + let family = resolveFamily(type);
76 if (family === undefined) {
77 // Check if we're dealing with a real forwardRef. Don't want to crash early.
78 if (
@@ -103,7 +109,7 @@ export function isCompatibleFamilyForHotReloading(
109 element: ReactElement,
110 ): boolean {
111 if (__DEV__) {
106 - if (familiesByType === null) {
112 + if (resolveFamily === null) {
113 // Hot reloading is disabled.
114 return false;
115 }
@@ -120,6 +126,12 @@ export function isCompatibleFamilyForHotReloading(
126 : null;
127
128 switch (fiber.tag) {
129 + case ClassComponent: {
130 + if (typeof nextType === 'function') {
131 + needsCompareFamilies = true;
132 + }
133 + break;
134 + }
135 case FunctionComponent: {
136 if (typeof nextType === 'function') {
137 needsCompareFamilies = true;
@@ -162,11 +174,8 @@ export function isCompatibleFamilyForHotReloading(
174 // If we unwrapped and compared the inner types for wrappers instead,
175 // then we would risk falsely saying two separate memo(Foo)
176 // calls are equivalent because they wrap the same Foo function.
165 - const prevFamily = familiesByType.get(prevType);
166 - if (
167 - prevFamily !== undefined &&
168 - prevFamily === familiesByType.get(nextType)
169 - ) {
177 + const prevFamily = resolveFamily(prevType);
178 + if (prevFamily !== undefined && prevFamily === resolveFamily(nextType)) {
179 return true;
180 }
181 }
@@ -178,7 +187,7 @@ export function isCompatibleFamilyForHotReloading(
187
188 export function markFailedErrorBoundaryForHotReloading(fiber: Fiber) {
189 if (__DEV__) {
181 - if (familiesByType === null) {
190 + if (resolveFamily === null) {
191 // Not hot reloading.
192 return;
193 }
@@ -195,7 +204,7 @@ export function markFailedErrorBoundaryForHotReloading(fiber: Fiber) {
204 export function scheduleHotUpdate(root: FiberRoot, hotUpdate: HotUpdate): void {
205 if (__DEV__) {
206 // TODO: warn if its identity changes over time?
198 - familiesByType = hotUpdate.familiesByType;
207 + resolveFamily = hotUpdate.resolveFamily;
208
209 const {staleFamilies, updatedFamilies} = hotUpdate;
210 flushPassiveEffects();
@@ -221,6 +230,7 @@ function scheduleFibersWithFamiliesRecursively(
230 switch (tag) {
231 case FunctionComponent:
232 case SimpleMemoComponent:
233 + case ClassComponent:
234 candidateType = type;
235 break;
236 case ForwardRef:
@@ -230,14 +240,14 @@ function scheduleFibersWithFamiliesRecursively(
240 break;
241 }
242
233 - if (familiesByType === null) {
234 - throw new Error('Expected familiesByType to be set during hot reload.');
243 + if (resolveFamily === null) {
244 + throw new Error('Expected resolveFamily to be set during hot reload.');
245 }
246
247 let needsRender = false;
248 let needsRemount = false;
249 if (candidateType !== null) {
240 - const family = familiesByType.get(candidateType);
250 + const family = resolveFamily(candidateType);
251 if (family !== undefined) {
252 if (staleFamilies.has(family)) {
253 needsRemount = true;