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