@samitouri / QOS-React-2 / commits / 12be8938a5

[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 +});