Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Dec 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/react-devtools-shared/src/hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@ export function installHook(target: any): DevToolsHook | null {
const hook: DevToolsHook = {
rendererInterfaces,
listeners,

// Fast Refresh for web relies on this.
renderers,

emit,
Expand Down
14 changes: 14 additions & 0 deletions packages/react-refresh/src/ReactFreshRuntime.js
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@ export function injectIntoGlobalHook(globalObject: any): void {
// Otherwise, the renderer will think that there is no global hook, and won't do the injection.
let nextID = 0;
globalObject.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook = {
renderers: new Map(),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just so the lines below don't crash when we take this code branch. This particular Map isn't actually used further.

supportsFiber: true,
inject(injected) {
return nextID++;
Expand Down Expand Up @@ -468,6 +469,19 @@ export function injectIntoGlobalHook(globalObject: any): void {
return id;
};

// Do the same for any already injected roots.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note this is almost copy paste from the code above (462–468). I could extract a function but meh.

// This is useful if ReactDOM has already been initialized.
// https://github.com/facebook/react/issues/17626
hook.renderers.forEach((injected, id) => {
if (
typeof injected.scheduleRefresh === 'function' &&
typeof injected.setRefreshHandler === 'function'
) {
gaearon marked this conversation as resolved.
Show resolved Hide resolved
// This version supports React Refresh.
helpersByRendererID.set(id, ((injected: any): RendererHelpers));
}
});

// We also want to track currently mounted roots.
const oldOnCommitFiberRoot = hook.onCommitFiberRoot;
const oldOnScheduleFiberRoot = hook.onScheduleFiberRoot || (() => {});
Expand Down
77 changes: 77 additions & 0 deletions packages/react-refresh/src/__tests__/ReactFresh-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('ReactFresh', () => {

afterEach(() => {
if (__DEV__) {
delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
document.body.removeChild(container);
}
});
Expand Down Expand Up @@ -3707,4 +3708,80 @@ describe('ReactFresh', () => {
// For example, we can use this to print a log of what was updated.
}
});

// This simulates the scenario in https://github.com/facebook/react/issues/17626.
it('can inject the runtime after the renderer executes', () => {
if (__DEV__) {
// This is a minimal shim for the global hook installed by DevTools.
// The real one is in packages/react-devtools-shared/src/hook.js.
let idCounter = 0;
let renderers = new Map();
global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
renderers,
supportsFiber: true,
inject(renderer) {
const id = ++idCounter;
renderers.set(id, renderer);
return id;
},
onCommitFiberRoot() {},
onCommitFiberUnmount() {},
};

// Load these first, as if they're coming from a CDN.
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
act = require('react-dom/test-utils').act;

// Important! Inject into the global hook *after* ReactDOM runs:
ReactFreshRuntime = require('react-refresh/runtime');
ReactFreshRuntime.injectIntoGlobalHook(global);

// We're verifying that we're able to track roots mounted after this point.
// The rest of this test is taken from the simplest first test case.

render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
return Hello;
});

// Bump the state before patching.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');

// Perform a hot update.
patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
return Hello;
});

// Assert the state was preserved but color changed.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
}
});
});