[Fresh] Support multiple renderers at the same time (#16302)
Dan Abramov committed
Aug 8, 2019 at 19:03 UTC
12be8938a5d71ffdc21ee7cf770bf1cb63ae038e
2 files changed
+161
-66
packages/react-refresh/src/ReactFreshRuntime.js
+62
-66
@@ -20,7 +20,6 @@ import type {
20
import type {ReactNodeList} from 'shared/ReactTypes';
21
22
import {REACT_MEMO_TYPE, REACT_FORWARD_REF_TYPE} from 'shared/ReactSymbols';
23
-import warningWithoutStack from 'shared/warningWithoutStack';
23
24
type Signature = {|
25
ownKey: string,
@@ -29,6 +28,13 @@ type Signature = {|
28
getCustomHooks: () => Array<Function>,
29
|};
30
31
+type RendererHelpers = {|
32
+ findHostInstancesForRefresh: FindHostInstancesForRefresh,
33
+ scheduleRefresh: ScheduleRefresh,
34
+ scheduleRoot: ScheduleRoot,
35
+ setRefreshHandler: SetRefreshHandler,
36
+|};
37
+
38
if (!__DEV__) {
39
throw new Error(
40
'React Refresh runtime should not be included in the production bundle.',
@@ -56,10 +62,9 @@ WeakMap<any, Family> | Map<any, Family> = new PossiblyWeakMap();
62
let pendingUpdates: Array<[Family, any]> = [];
63
64
// 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();
68
69
// We keep track of mounted roots so we can schedule updates.
70
let mountedRoots: Set<FiberRoot> = new Set();
@@ -182,49 +187,23 @@ export function performReactRefresh(): RefreshUpdate | null {
187
staleFamilies, // Families that will be remounted
188
};
189
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
+ });
195
196
let didError = false;
197
let firstError = null;
198
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
+ }
205
try {
227
- scheduleRenderForRoot(root, element);
206
+ helpers.scheduleRoot(root, element);
207
} catch (err) {
208
if (!didError) {
209
didError = true;
@@ -234,8 +213,14 @@ export function performReactRefresh(): RefreshUpdate | null {
213
}
214
});
215
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
+ }
222
try {
238
- scheduleRefreshForRoot(root, update);
223
+ helpers.scheduleRefresh(root, update);
224
} catch (err) {
225
if (!didError) {
226
didError = true;
@@ -359,20 +344,18 @@ export function findAffectedHostInstances(
344
families: Array<Family>,
345
): Set<Instance> {
346
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;
347
let affectedInstances = new Set();
348
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
+ );
359
instancesForRoot.forEach(inst => {
360
affectedInstances.add(inst);
361
});
@@ -397,11 +380,14 @@ export function injectIntoGlobalHook(globalObject: any): void {
380
// However, if there is no DevTools extension, we'll need to set up the global hook ourselves.
381
// Note that in this case it's important that renderer code runs *after* this method call.
382
// Otherwise, the renderer will think that there is no global hook, and won't do the injection.
383
+ let nextID = 0;
384
globalObject.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook = {
385
supportsFiber: true,
402
- inject() {},
386
+ inject(injected) {
387
+ return nextID++;
388
+ },
389
onCommitFiberRoot(
404
- id: mixed,
390
+ id: number,
391
root: FiberRoot,
392
maybePriorityLevel: mixed,
393
didError: boolean,
@@ -413,23 +399,31 @@ export function injectIntoGlobalHook(globalObject: any): void {
399
// Here, we just want to get a reference to scheduleRefresh.
400
const oldInject = hook.inject;
401
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;
411
};
412
413
// We also want to track currently mounted roots.
414
const oldOnCommitFiberRoot = hook.onCommitFiberRoot;
415
hook.onCommitFiberRoot = function(
428
- id: mixed,
416
+ id: number,
417
root: FiberRoot,
418
maybePriorityLevel: mixed,
419
didError: boolean,
420
) {
421
+ const helpers = helpersByRendererID.get(id);
422
+ if (helpers === undefined) {
423
+ return;
424
+ }
425
+ helpersByRoot.set(root, helpers);
426
+
427
const current = root.current;
428
const alternate = current.alternate;
429
@@ -459,6 +453,8 @@ export function injectIntoGlobalHook(globalObject: any): void {
453
// We'll remount it on future edits.
454
// Remember what was rendered so we can restore it.
455
failedRoots.set(root, alternate.memoizedState.element);
456
+ } else {
457
+ helpersByRoot.delete(root);
458
}
459
} else if (!wasMounted && !isMounted) {
460
if (didError && !failedRoots.has(root)) {
packages/react-refresh/src/__tests__/ReactFreshMultipleRenderer-test.internal.js
new
+99
@@ -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
+});