[Fresh] Support multiple renderers at the same time (#16302)

This commit is contained in:
Dan Abramov
2019-08-08 19:03:38 +01:00
committed by GitHub
parent 6f3c8332d8
commit 12be8938a5
2 changed files with 161 additions and 66 deletions
+62 -66
View File
@@ -20,7 +20,6 @@ import type {
import type {ReactNodeList} from 'shared/ReactTypes';
import {REACT_MEMO_TYPE, REACT_FORWARD_REF_TYPE} from 'shared/ReactSymbols';
import warningWithoutStack from 'shared/warningWithoutStack';
type Signature = {|
ownKey: string,
@@ -29,6 +28,13 @@ type Signature = {|
getCustomHooks: () => Array<Function>,
|};
type RendererHelpers = {|
findHostInstancesForRefresh: FindHostInstancesForRefresh,
scheduleRefresh: ScheduleRefresh,
scheduleRoot: ScheduleRoot,
setRefreshHandler: SetRefreshHandler,
|};
if (!__DEV__) {
throw new Error(
'React Refresh runtime should not be included in the production bundle.',
@@ -56,10 +62,9 @@ WeakMap<any, Family> | Map<any, Family> = new PossiblyWeakMap();
let pendingUpdates: Array<[Family, any]> = [];
// This is injected by the renderer via DevTools global hook.
let setRefreshHandler: null | SetRefreshHandler = null;
let scheduleRefresh: null | ScheduleRefresh = null;
let scheduleRoot: null | ScheduleRoot = null;
let findHostInstancesForRefresh: null | FindHostInstancesForRefresh = null;
let helpersByRendererID: Map<number, RendererHelpers> = new Map();
let helpersByRoot: Map<FiberRoot, RendererHelpers> = new Map();
// We keep track of mounted roots so we can schedule updates.
let mountedRoots: Set<FiberRoot> = new Set();
@@ -182,49 +187,23 @@ export function performReactRefresh(): RefreshUpdate | null {
staleFamilies, // Families that will be remounted
};
if (typeof setRefreshHandler !== 'function') {
warningWithoutStack(
false,
'Could not find the setRefreshHandler() implementation. ' +
'This likely means that injectIntoGlobalHook() was either ' +
'called before the global DevTools hook was set up, or after the ' +
'renderer has already initialized. Please file an issue with a reproducing case.',
);
return null;
}
if (typeof scheduleRefresh !== 'function') {
warningWithoutStack(
false,
'Could not find the scheduleRefresh() implementation. ' +
'This likely means that injectIntoGlobalHook() was either ' +
'called before the global DevTools hook was set up, or after the ' +
'renderer has already initialized. Please file an issue with a reproducing case.',
);
return null;
}
if (typeof scheduleRoot !== 'function') {
warningWithoutStack(
false,
'Could not find the scheduleRoot() implementation. ' +
'This likely means that injectIntoGlobalHook() was either ' +
'called before the global DevTools hook was set up, or after the ' +
'renderer has already initialized. Please file an issue with a reproducing case.',
);
return null;
}
const scheduleRefreshForRoot = scheduleRefresh;
const scheduleRenderForRoot = scheduleRoot;
// Even if there are no roots, set the handler on first update.
// This ensures that if *new* roots are mounted, they'll use the resolve handler.
setRefreshHandler(resolveFamily);
helpersByRendererID.forEach(helpers => {
// Even if there are no roots, set the handler on first update.
// This ensures that if *new* roots are mounted, they'll use the resolve handler.
helpers.setRefreshHandler(resolveFamily);
});
let didError = false;
let firstError = null;
failedRoots.forEach((element, root) => {
const helpers = helpersByRoot.get(root);
if (helpers === undefined) {
throw new Error(
'Could not find helpers for a root. This is a bug in React Refresh.',
);
}
try {
scheduleRenderForRoot(root, element);
helpers.scheduleRoot(root, element);
} catch (err) {
if (!didError) {
didError = true;
@@ -234,8 +213,14 @@ export function performReactRefresh(): RefreshUpdate | null {
}
});
mountedRoots.forEach(root => {
const helpers = helpersByRoot.get(root);
if (helpers === undefined) {
throw new Error(
'Could not find helpers for a root. This is a bug in React Refresh.',
);
}
try {
scheduleRefreshForRoot(root, update);
helpers.scheduleRefresh(root, update);
} catch (err) {
if (!didError) {
didError = true;
@@ -359,20 +344,18 @@ export function findAffectedHostInstances(
families: Array<Family>,
): Set<Instance> {
if (__DEV__) {
if (typeof findHostInstancesForRefresh !== 'function') {
warningWithoutStack(
false,
'Could not find the findHostInstancesForRefresh() implementation. ' +
'This likely means that injectIntoGlobalHook() was either ' +
'called before the global DevTools hook was set up, or after the ' +
'renderer has already initialized. Please file an issue with a reproducing case.',
);
return new Set();
}
const findInstances = findHostInstancesForRefresh;
let affectedInstances = new Set();
mountedRoots.forEach(root => {
const instancesForRoot = findInstances(root, families);
const helpers = helpersByRoot.get(root);
if (helpers === undefined) {
throw new Error(
'Could not find helpers for a root. This is a bug in React Refresh.',
);
}
const instancesForRoot = helpers.findHostInstancesForRefresh(
root,
families,
);
instancesForRoot.forEach(inst => {
affectedInstances.add(inst);
});
@@ -397,11 +380,14 @@ export function injectIntoGlobalHook(globalObject: any): void {
// However, if there is no DevTools extension, we'll need to set up the global hook ourselves.
// Note that in this case it's important that renderer code runs *after* this method call.
// 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 = {
supportsFiber: true,
inject() {},
inject(injected) {
return nextID++;
},
onCommitFiberRoot(
id: mixed,
id: number,
root: FiberRoot,
maybePriorityLevel: mixed,
didError: boolean,
@@ -413,23 +399,31 @@ export function injectIntoGlobalHook(globalObject: any): void {
// Here, we just want to get a reference to scheduleRefresh.
const oldInject = hook.inject;
hook.inject = function(injected) {
findHostInstancesForRefresh = ((injected: any)
.findHostInstancesForRefresh: FindHostInstancesForRefresh);
scheduleRefresh = ((injected: any).scheduleRefresh: ScheduleRefresh);
scheduleRoot = ((injected: any).scheduleRoot: ScheduleRoot);
setRefreshHandler = ((injected: any)
.setRefreshHandler: SetRefreshHandler);
return oldInject.apply(this, arguments);
const id = oldInject.apply(this, arguments);
if (
typeof injected.scheduleRefresh === 'function' &&
typeof injected.setRefreshHandler === 'function'
) {
// This version supports React Refresh.
helpersByRendererID.set(id, ((injected: any): RendererHelpers));
}
return id;
};
// We also want to track currently mounted roots.
const oldOnCommitFiberRoot = hook.onCommitFiberRoot;
hook.onCommitFiberRoot = function(
id: mixed,
id: number,
root: FiberRoot,
maybePriorityLevel: mixed,
didError: boolean,
) {
const helpers = helpersByRendererID.get(id);
if (helpers === undefined) {
return;
}
helpersByRoot.set(root, helpers);
const current = root.current;
const alternate = current.alternate;
@@ -459,6 +453,8 @@ export function injectIntoGlobalHook(globalObject: any): void {
// We'll remount it on future edits.
// Remember what was rendered so we can restore it.
failedRoots.set(root, alternate.memoizedState.element);
} else {
helpersByRoot.delete(root);
}
} else if (!wasMounted && !isMounted) {
if (didError && !failedRoots.has(root)) {
@@ -0,0 +1,99 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
jest.resetModules();
let React = require('react');
let ReactFreshRuntime;
if (__DEV__) {
ReactFreshRuntime = require('react-refresh/runtime');
ReactFreshRuntime.injectIntoGlobalHook(global);
}
let ReactDOM = require('react-dom');
jest.resetModules();
let ReactART = require('react-art');
let ARTSVGMode = require('art/modes/svg');
let ARTCurrentMode = require('art/modes/current');
ARTCurrentMode.setCurrent(ARTSVGMode);
describe('ReactFresh', () => {
let container;
beforeEach(() => {
if (__DEV__) {
container = document.createElement('div');
document.body.appendChild(container);
}
});
afterEach(() => {
if (__DEV__) {
document.body.removeChild(container);
container = null;
}
});
it('can update components managd by different renderers independently', () => {
if (__DEV__) {
let InnerV1 = function() {
return <ReactART.Shape fill="blue" />;
};
ReactFreshRuntime.register(InnerV1, 'Inner');
let OuterV1 = function() {
return (
<div style={{color: 'blue'}}>
<ReactART.Surface>
<InnerV1 />
</ReactART.Surface>
</div>
);
};
ReactFreshRuntime.register(OuterV1, 'Outer');
ReactDOM.render(<OuterV1 />, container);
const el = container.firstChild;
const pathEl = el.querySelector('path');
expect(el.style.color).toBe('blue');
expect(pathEl.getAttributeNS(null, 'fill')).toBe('rgb(0, 0, 255)');
// Perform a hot update to the ART-rendered component.
let InnerV2 = function() {
return <ReactART.Shape fill="red" />;
};
ReactFreshRuntime.register(InnerV2, 'Inner');
ReactFreshRuntime.performReactRefresh();
expect(container.firstChild).toBe(el);
expect(el.querySelector('path')).toBe(pathEl);
expect(el.style.color).toBe('blue');
expect(pathEl.getAttributeNS(null, 'fill')).toBe('rgb(255, 0, 0)');
// Perform a hot update to the DOM-rendered component.
let OuterV2 = function() {
return (
<div style={{color: 'red'}}>
<ReactART.Surface>
<InnerV1 />
</ReactART.Surface>
</div>
);
};
ReactFreshRuntime.register(OuterV2, 'Outer');
ReactFreshRuntime.performReactRefresh();
expect(el.style.color).toBe('red');
expect(container.firstChild).toBe(el);
expect(el.querySelector('path')).toBe(pathEl);
expect(pathEl.getAttributeNS(null, 'fill')).toBe('rgb(255, 0, 0)');
}
});
});