[Fresh] Fix edge case with early function call (#17824)
Dan Abramov committed
Jan 12, 2020 at 17:53 UTC
255d9ac5f58e85903e27128cf263dd6d2f9e7133
2 files changed
+101
-7
packages/react-refresh/src/ReactFreshRuntime.js
+21
-7
@@ -601,9 +601,14 @@ export function _getMountedRootCount() {
601
// 'useState{[foo, setFoo]}(0)',
602
// () => [useCustomHook], /* Lazy to avoid triggering inline requires */
603
// );
604
+type SignatureStatus = 'needsSignature' | 'needsCustomHooks' | 'resolved';
605
export function createSignatureFunctionForTransform() {
606
if (__DEV__) {
606
- let call = 0;
607
+ // We'll fill in the signature in two steps.
608
+ // First, we'll know the signature itself. This happens outside the component.
609
+ // Then, we'll know the references to custom Hooks. This happens inside the component.
610
+ // After that, the returned function will be a fast path no-op.
611
+ let status: SignatureStatus = 'needsSignature';
612
let savedType;
613
let hasCustomHooks;
614
return function<T>(
@@ -612,16 +617,25 @@ export function createSignatureFunctionForTransform() {
617
forceReset?: boolean,
618
getCustomHooks?: () => Array<Function>,
619
): T {
615
- switch (call++) {
616
- case 0:
617
- savedType = type;
618
- hasCustomHooks = typeof getCustomHooks === 'function';
619
- setSignature(type, key, forceReset, getCustomHooks);
620
+ switch (status) {
621
+ case 'needsSignature':
622
+ if (type !== undefined) {
623
+ // If we received an argument, this is the initial registration call.
624
+ savedType = type;
625
+ hasCustomHooks = typeof getCustomHooks === 'function';
626
+ setSignature(type, key, forceReset, getCustomHooks);
627
+ // The next call we expect is from inside a function, to fill in the custom Hooks.
628
+ status = 'needsCustomHooks';
629
+ }
630
break;
621
- case 1:
631
+ case 'needsCustomHooks':
632
if (hasCustomHooks) {
633
collectCustomHooksForSignature(savedType);
634
}
635
+ status = 'resolved';
636
+ break;
637
+ case 'resolved':
638
+ // Do nothing. Fast path for all future renders.
639
break;
640
}
641
return type;
packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js
+80
@@ -605,6 +605,86 @@ describe('ReactFreshIntegration', () => {
605
}
606
});
607
608
+ it('does not get confused when component is called early', () => {
609
+ if (__DEV__) {
610
+ render(`
611
+ // This isn't really a valid pattern but it's close enough
612
+ // to simulate what happens when you call ReactDOM.render
613
+ // in the same file. We want to ensure this doesn't confuse
614
+ // the runtime.
615
+ App();
616
+
617
+ function App() {
618
+ const [x, setX] = useFancyState('X');
619
+ const [y, setY] = useFancyState('Y');
620
+ return <h1>A{x}{y}</h1>;
621
+ };
622
+
623
+ function useFancyState(initialState) {
624
+ // No real Hook calls to avoid triggering invalid call invariant.
625
+ // We only want to verify that we can still call this function early.
626
+ return initialState;
627
+ }
628
+
629
+ export default App;
630
+ `);
631
+ let el = container.firstChild;
632
+ expect(el.textContent).toBe('AXY');
633
+
634
+ patch(`
635
+ // This isn't really a valid pattern but it's close enough
636
+ // to simulate what happens when you call ReactDOM.render
637
+ // in the same file. We want to ensure this doesn't confuse
638
+ // the runtime.
639
+ App();
640
+
641
+ function App() {
642
+ const [x, setX] = useFancyState('X');
643
+ const [y, setY] = useFancyState('Y');
644
+ return <h1>B{x}{y}</h1>;
645
+ };
646
+
647
+ function useFancyState(initialState) {
648
+ // No real Hook calls to avoid triggering invalid call invariant.
649
+ // We only want to verify that we can still call this function early.
650
+ return initialState;
651
+ }
652
+
653
+ export default App;
654
+ `);
655
+ // Same state variables, so no remount.
656
+ expect(container.firstChild).toBe(el);
657
+ expect(el.textContent).toBe('BXY');
658
+
659
+ patch(`
660
+ // This isn't really a valid pattern but it's close enough
661
+ // to simulate what happens when you call ReactDOM.render
662
+ // in the same file. We want to ensure this doesn't confuse
663
+ // the runtime.
664
+ App();
665
+
666
+ function App() {
667
+ const [y, setY] = useFancyState('Y');
668
+ const [x, setX] = useFancyState('X');
669
+ return <h1>B{x}{y}</h1>;
670
+ };
671
+
672
+ function useFancyState(initialState) {
673
+ // No real Hook calls to avoid triggering invalid call invariant.
674
+ // We only want to verify that we can still call this function early.
675
+ return initialState;
676
+ }
677
+
678
+ export default App;
679
+ `);
680
+ // Hooks were re-ordered. This causes a remount.
681
+ // Therefore, Hook calls don't accidentally share state.
682
+ expect(container.firstChild).not.toBe(el);
683
+ el = container.firstChild;
684
+ expect(el.textContent).toBe('BXY');
685
+ }
686
+ });
687
+
688
it('does not get confused by Hooks defined inline', () => {
689
// This is not a recommended pattern but at least it shouldn't break.
690
if (__DEV__) {