@samitouri / QOS-React / commits / 255d9ac5f5

[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__) {