[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;