Skip to content

Commit 12be893

Browse files
authored
[Fresh] Support multiple renderers at the same time (#16302)
1 parent 6f3c833 commit 12be893

File tree

2 files changed

+161
-66
lines changed

2 files changed

+161
-66
lines changed

packages/react-refresh/src/ReactFreshRuntime.js

Lines changed: 62 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import type {
2020
import type {ReactNodeList} from 'shared/ReactTypes';
2121

2222
import {REACT_MEMO_TYPE, REACT_FORWARD_REF_TYPE} from 'shared/ReactSymbols';
23-
import warningWithoutStack from 'shared/warningWithoutStack';
2423

2524
type Signature = {|
2625
ownKey: string,
@@ -29,6 +28,13 @@ type Signature = {|
2928
getCustomHooks: () => Array<Function>,
3029
|};
3130

31+
type RendererHelpers = {|
32+
findHostInstancesForRefresh: FindHostInstancesForRefresh,
33+
scheduleRefresh: ScheduleRefresh,
34+
scheduleRoot: ScheduleRoot,
35+
setRefreshHandler: SetRefreshHandler,
36+
|};
37+
3238
if (!__DEV__) {
3339
throw new Error(
3440
'React Refresh runtime should not be included in the production bundle.',
@@ -56,10 +62,9 @@ WeakMap<any, Family> | Map<any, Family> = new PossiblyWeakMap();
5662
let pendingUpdates: Array<[Family, any]> = [];
5763

5864
// This is injected by the renderer via DevTools global hook.
59-
let setRefreshHandler: null | SetRefreshHandler = null;
60-
let scheduleRefresh: null | ScheduleRefresh = null;
61-
let scheduleRoot: null | ScheduleRoot = null;
62-
let findHostInstancesForRefresh: null | FindHostInstancesForRefresh = null;
65+
let helpersByRendererID: Map<number, RendererHelpers> = new Map();
66+
67+
let helpersByRoot: Map<FiberRoot, RendererHelpers> = new Map();
6368

6469
// We keep track of mounted roots so we can schedule updates.
6570
let mountedRoots: Set<FiberRoot> = new Set();
@@ -182,49 +187,23 @@ export function performReactRefresh(): RefreshUpdate | null {
182187
staleFamilies, // Families that will be remounted
183188
};
184189

185-
if (typeof setRefreshHandler !== 'function') {
186-
warningWithoutStack(
187-
false,
188-
'Could not find the setRefreshHandler() implementation. ' +
189-
'This likely means that injectIntoGlobalHook() was either ' +
190-
'called before the global DevTools hook was set up, or after the ' +
191-
'renderer has already initialized. Please file an issue with a reproducing case.',
192-
);
193-
return null;
194-
}
195-
196-
if (typeof scheduleRefresh !== 'function') {
197-
warningWithoutStack(
198-
false,
199-
'Could not find the scheduleRefresh() implementation. ' +
200-
'This likely means that injectIntoGlobalHook() was either ' +
201-
'called before the global DevTools hook was set up, or after the ' +
202-
'renderer has already initialized. Please file an issue with a reproducing case.',
203-
);
204-
return null;
205-
}
206-
if (typeof scheduleRoot !== 'function') {
207-
warningWithoutStack(
208-
false,
209-
'Could not find the scheduleRoot() implementation. ' +
210-
'This likely means that injectIntoGlobalHook() was either ' +
211-
'called before the global DevTools hook was set up, or after the ' +
212-
'renderer has already initialized. Please file an issue with a reproducing case.',
213-
);
214-
return null;
215-
}
216-
const scheduleRefreshForRoot = scheduleRefresh;
217-
const scheduleRenderForRoot = scheduleRoot;
218-
219-
// Even if there are no roots, set the handler on first update.
220-
// This ensures that if *new* roots are mounted, they'll use the resolve handler.
221-
setRefreshHandler(resolveFamily);
190+
helpersByRendererID.forEach(helpers => {
191+
// Even if there are no roots, set the handler on first update.
192+
// This ensures that if *new* roots are mounted, they'll use the resolve handler.
193+
helpers.setRefreshHandler(resolveFamily);
194+
});
222195

223196
let didError = false;
224197
let firstError = null;
225198
failedRoots.forEach((element, root) => {
199+
const helpers = helpersByRoot.get(root);
200+
if (helpers === undefined) {
201+
throw new Error(
202+
'Could not find helpers for a root. This is a bug in React Refresh.',
203+
);
204+
}
226205
try {
227-
scheduleRenderForRoot(root, element);
206+
helpers.scheduleRoot(root, element);
228207
} catch (err) {
229208
if (!didError) {
230209
didError = true;
@@ -234,8 +213,14 @@ export function performReactRefresh(): RefreshUpdate | null {
234213
}
235214
});
236215
mountedRoots.forEach(root => {
216+
const helpers = helpersByRoot.get(root);
217+
if (helpers === undefined) {
218+
throw new Error(
219+
'Could not find helpers for a root. This is a bug in React Refresh.',
220+
);
221+
}
237222
try {
238-
scheduleRefreshForRoot(root, update);
223+
helpers.scheduleRefresh(root, update);
239224
} catch (err) {
240225
if (!didError) {
241226
didError = true;
@@ -359,20 +344,18 @@ export function findAffectedHostInstances(
359344
families: Array<Family>,
360345
): Set<Instance> {
361346
if (__DEV__) {
362-
if (typeof findHostInstancesForRefresh !== 'function') {
363-
warningWithoutStack(
364-
false,
365-
'Could not find the findHostInstancesForRefresh() implementation. ' +
366-
'This likely means that injectIntoGlobalHook() was either ' +
367-
'called before the global DevTools hook was set up, or after the ' +
368-
'renderer has already initialized. Please file an issue with a reproducing case.',
369-
);
370-
return new Set();
371-
}
372-
const findInstances = findHostInstancesForRefresh;
373347
let affectedInstances = new Set();
374348
mountedRoots.forEach(root => {
375-
const instancesForRoot = findInstances(root, families);
349+
const helpers = helpersByRoot.get(root);
350+
if (helpers === undefined) {
351+
throw new Error(
352+
'Could not find helpers for a root. This is a bug in React Refresh.',
353+
);
354+
}
355+
const instancesForRoot = helpers.findHostInstancesForRefresh(
356+
root,
357+
families,
358+
);
376359
instancesForRoot.forEach(inst => {
377360
affectedInstances.add(inst);
378361
});
@@ -397,11 +380,14 @@ export function injectIntoGlobalHook(globalObject: any): void {
397380
// However, if there is no DevTools extension, we'll need to set up the global hook ourselves.
398381
// Note that in this case it's important that renderer code runs *after* this method call.
399382
// Otherwise, the renderer will think that there is no global hook, and won't do the injection.
383+
let nextID = 0;
400384
globalObject.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook = {
401385
supportsFiber: true,
402-
inject() {},
386+
inject(injected) {
387+
return nextID++;
388+
},
403389
onCommitFiberRoot(
404-
id: mixed,
390+
id: number,
405391
root: FiberRoot,
406392
maybePriorityLevel: mixed,
407393
didError: boolean,
@@ -413,23 +399,31 @@ export function injectIntoGlobalHook(globalObject: any): void {
413399
// Here, we just want to get a reference to scheduleRefresh.
414400
const oldInject = hook.inject;
415401
hook.inject = function(injected) {
416-
findHostInstancesForRefresh = ((injected: any)
417-
.findHostInstancesForRefresh: FindHostInstancesForRefresh);
418-
scheduleRefresh = ((injected: any).scheduleRefresh: ScheduleRefresh);
419-
scheduleRoot = ((injected: any).scheduleRoot: ScheduleRoot);
420-
setRefreshHandler = ((injected: any)
421-
.setRefreshHandler: SetRefreshHandler);
422-
return oldInject.apply(this, arguments);
402+
const id = oldInject.apply(this, arguments);
403+
if (
404+
typeof injected.scheduleRefresh === 'function' &&
405+
typeof injected.setRefreshHandler === 'function'
406+
) {
407+
// This version supports React Refresh.
408+
helpersByRendererID.set(id, ((injected: any): RendererHelpers));
409+
}
410+
return id;
423411
};
424412

425413
// We also want to track currently mounted roots.
426414
const oldOnCommitFiberRoot = hook.onCommitFiberRoot;
427415
hook.onCommitFiberRoot = function(
428-
id: mixed,
416+
id: number,
429417
root: FiberRoot,
430418
maybePriorityLevel: mixed,
431419
didError: boolean,
432420
) {
421+
const helpers = helpersByRendererID.get(id);
422+
if (helpers === undefined) {
423+
return;
424+
}
425+
helpersByRoot.set(root, helpers);
426+
433427
const current = root.current;
434428
const alternate = current.alternate;
435429

@@ -459,6 +453,8 @@ export function injectIntoGlobalHook(globalObject: any): void {
459453
// We'll remount it on future edits.
460454
// Remember what was rendered so we can restore it.
461455
failedRoots.set(root, alternate.memoizedState.element);
456+
} else {
457+
helpersByRoot.delete(root);
462458
}
463459
} else if (!wasMounted && !isMounted) {
464460
if (didError && !failedRoots.has(root)) {
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails react-core
8+
*/
9+
10+
'use strict';
11+
12+
jest.resetModules();
13+
let React = require('react');
14+
let ReactFreshRuntime;
15+
if (__DEV__) {
16+
ReactFreshRuntime = require('react-refresh/runtime');
17+
ReactFreshRuntime.injectIntoGlobalHook(global);
18+
}
19+
let ReactDOM = require('react-dom');
20+
21+
jest.resetModules();
22+
let ReactART = require('react-art');
23+
let ARTSVGMode = require('art/modes/svg');
24+
let ARTCurrentMode = require('art/modes/current');
25+
ARTCurrentMode.setCurrent(ARTSVGMode);
26+
27+
describe('ReactFresh', () => {
28+
let container;
29+
30+
beforeEach(() => {
31+
if (__DEV__) {
32+
container = document.createElement('div');
33+
document.body.appendChild(container);
34+
}
35+
});
36+
37+
afterEach(() => {
38+
if (__DEV__) {
39+
document.body.removeChild(container);
40+
container = null;
41+
}
42+
});
43+
44+
it('can update components managd by different renderers independently', () => {
45+
if (__DEV__) {
46+
let InnerV1 = function() {
47+
return <ReactART.Shape fill="blue" />;
48+
};
49+
ReactFreshRuntime.register(InnerV1, 'Inner');
50+
51+
let OuterV1 = function() {
52+
return (
53+
<div style={{color: 'blue'}}>
54+
<ReactART.Surface>
55+
<InnerV1 />
56+
</ReactART.Surface>
57+
</div>
58+
);
59+
};
60+
ReactFreshRuntime.register(OuterV1, 'Outer');
61+
62+
ReactDOM.render(<OuterV1 />, container);
63+
const el = container.firstChild;
64+
const pathEl = el.querySelector('path');
65+
expect(el.style.color).toBe('blue');
66+
expect(pathEl.getAttributeNS(null, 'fill')).toBe('rgb(0, 0, 255)');
67+
68+
// Perform a hot update to the ART-rendered component.
69+
let InnerV2 = function() {
70+
return <ReactART.Shape fill="red" />;
71+
};
72+
ReactFreshRuntime.register(InnerV2, 'Inner');
73+
74+
ReactFreshRuntime.performReactRefresh();
75+
expect(container.firstChild).toBe(el);
76+
expect(el.querySelector('path')).toBe(pathEl);
77+
expect(el.style.color).toBe('blue');
78+
expect(pathEl.getAttributeNS(null, 'fill')).toBe('rgb(255, 0, 0)');
79+
80+
// Perform a hot update to the DOM-rendered component.
81+
let OuterV2 = function() {
82+
return (
83+
<div style={{color: 'red'}}>
84+
<ReactART.Surface>
85+
<InnerV1 />
86+
</ReactART.Surface>
87+
</div>
88+
);
89+
};
90+
ReactFreshRuntime.register(OuterV2, 'Outer');
91+
92+
ReactFreshRuntime.performReactRefresh();
93+
expect(el.style.color).toBe('red');
94+
expect(container.firstChild).toBe(el);
95+
expect(el.querySelector('path')).toBe(pathEl);
96+
expect(pathEl.getAttributeNS(null, 'fill')).toBe('rgb(255, 0, 0)');
97+
}
98+
});
99+
});

0 commit comments

Comments
 (0)