mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[Fresh] Support multiple renderers at the same time (#16302)
This commit is contained in:
+62
-66
@@ -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)');
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user