Skip to content

Commit c2d1561

Browse files
authored
[Fast Refresh] Support injecting runtime after renderer executes (#17633)
1 parent 0253ee9 commit c2d1561

File tree

3 files changed

+93
-0
lines changed

3 files changed

+93
-0
lines changed

packages/react-devtools-shared/src/hook.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,8 @@ export function installHook(target: any): DevToolsHook | null {
277277
const hook: DevToolsHook = {
278278
rendererInterfaces,
279279
listeners,
280+
281+
// Fast Refresh for web relies on this.
280282
renderers,
281283

282284
emit,

packages/react-refresh/src/ReactFreshRuntime.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ export function injectIntoGlobalHook(globalObject: any): void {
435435
// Otherwise, the renderer will think that there is no global hook, and won't do the injection.
436436
let nextID = 0;
437437
globalObject.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook = {
438+
renderers: new Map(),
438439
supportsFiber: true,
439440
inject(injected) {
440441
return nextID++;
@@ -468,6 +469,19 @@ export function injectIntoGlobalHook(globalObject: any): void {
468469
return id;
469470
};
470471

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+
471485
// We also want to track currently mounted roots.
472486
const oldOnCommitFiberRoot = hook.onCommitFiberRoot;
473487
const oldOnScheduleFiberRoot = hook.onScheduleFiberRoot || (() => {});

packages/react-refresh/src/__tests__/ReactFresh-test.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ describe('ReactFresh', () => {
4242

4343
afterEach(() => {
4444
if (__DEV__) {
45+
delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
4546
document.body.removeChild(container);
4647
}
4748
});
@@ -3707,4 +3708,80 @@ describe('ReactFresh', () => {
37073708
// For example, we can use this to print a log of what was updated.
37083709
}
37093710
});
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+
});
37103787
});

0 commit comments

Comments
 (0)