@samitouri / QOS-React / commits / c2d1561c60

[Fast Refresh] Support injecting runtime after renderer executes (#17633)

Dan Abramov committed Dec 17, 2019 at 13:49 UTC c2d1561c60507189b43ae96c6b89580874367e36
3 files changed +93
packages/react-devtools-shared/src/hook.js
+2
@@ -277,6 +277,8 @@ export function installHook(target: any): DevToolsHook | null {
277 const hook: DevToolsHook = {
278 rendererInterfaces,
279 listeners,
280 +
281 + // Fast Refresh for web relies on this.
282 renderers,
283
284 emit,
packages/react-refresh/src/ReactFreshRuntime.js
+14
@@ -435,6 +435,7 @@ export function injectIntoGlobalHook(globalObject: any): void {
435 // Otherwise, the renderer will think that there is no global hook, and won't do the injection.
436 let nextID = 0;
437 globalObject.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook = {
438 + renderers: new Map(),
439 supportsFiber: true,
440 inject(injected) {
441 return nextID++;
@@ -468,6 +469,19 @@ export function injectIntoGlobalHook(globalObject: any): void {
469 return id;
470 };
471
472 + // Do the same for any already injected roots.
473 + // This is useful if ReactDOM has already been initialized.
474 + // https://github.com/facebook/react/issues/17626
475 + hook.renderers.forEach((injected, id) => {
476 + if (
477 + typeof injected.scheduleRefresh === 'function' &&
478 + typeof injected.setRefreshHandler === 'function'
479 + ) {
480 + // This version supports React Refresh.
481 + helpersByRendererID.set(id, ((injected: any): RendererHelpers));
482 + }
483 + });
484 +
485 // We also want to track currently mounted roots.
486 const oldOnCommitFiberRoot = hook.onCommitFiberRoot;
487 const oldOnScheduleFiberRoot = hook.onScheduleFiberRoot || (() => {});
packages/react-refresh/src/__tests__/ReactFresh-test.js
+77
@@ -42,6 +42,7 @@ describe('ReactFresh', () => {
42
43 afterEach(() => {
44 if (__DEV__) {
45 + delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
46 document.body.removeChild(container);
47 }
48 });
@@ -3707,4 +3708,80 @@ describe('ReactFresh', () => {
3708 // For example, we can use this to print a log of what was updated.
3709 }
3710 });
3711 +
3712 + // This simulates the scenario in https://github.com/facebook/react/issues/17626.
3713 + it('can inject the runtime after the renderer executes', () => {
3714 + if (__DEV__) {
3715 + // This is a minimal shim for the global hook installed by DevTools.
3716 + // The real one is in packages/react-devtools-shared/src/hook.js.
3717 + let idCounter = 0;
3718 + let renderers = new Map();
3719 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
3720 + renderers,
3721 + supportsFiber: true,
3722 + inject(renderer) {
3723 + const id = ++idCounter;
3724 + renderers.set(id, renderer);
3725 + return id;
3726 + },
3727 + onCommitFiberRoot() {},
3728 + onCommitFiberUnmount() {},
3729 + };
3730 +
3731 + // Load these first, as if they're coming from a CDN.
3732 + jest.resetModules();
3733 + React = require('react');
3734 + ReactDOM = require('react-dom');
3735 + Scheduler = require('scheduler');
3736 + act = require('react-dom/test-utils').act;
3737 +
3738 + // Important! Inject into the global hook *after* ReactDOM runs:
3739 + ReactFreshRuntime = require('react-refresh/runtime');
3740 + ReactFreshRuntime.injectIntoGlobalHook(global);
3741 +
3742 + // We're verifying that we're able to track roots mounted after this point.
3743 + // The rest of this test is taken from the simplest first test case.
3744 +
3745 + render(() => {
3746 + function Hello() {
3747 + const [val, setVal] = React.useState(0);
3748 + return (
3749 + <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
3750 + {val}
3751 + </p>
3752 + );
3753 + }
3754 + $RefreshReg$(Hello, 'Hello');
3755 + return Hello;
3756 + });
3757 +
3758 + // Bump the state before patching.
3759 + const el = container.firstChild;
3760 + expect(el.textContent).toBe('0');
3761 + expect(el.style.color).toBe('blue');
3762 + act(() => {
3763 + el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
3764 + });
3765 + expect(el.textContent).toBe('1');
3766 +
3767 + // Perform a hot update.
3768 + patch(() => {
3769 + function Hello() {
3770 + const [val, setVal] = React.useState(0);
3771 + return (
3772 + <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
3773 + {val}
3774 + </p>
3775 + );
3776 + }
3777 + $RefreshReg$(Hello, 'Hello');
3778 + return Hello;
3779 + });
3780 +
3781 + // Assert the state was preserved but color changed.
3782 + expect(container.firstChild).toBe(el);
3783 + expect(el.textContent).toBe('1');
3784 + expect(el.style.color).toBe('red');
3785 + }
3786 + });
3787 });