diff --git a/packages/react-refresh/src/ReactFreshRuntime.js b/packages/react-refresh/src/ReactFreshRuntime.js index fe7328541016a..73e4747fbf588 100644 --- a/packages/react-refresh/src/ReactFreshRuntime.js +++ b/packages/react-refresh/src/ReactFreshRuntime.js @@ -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, |}; +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 | Map = 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 = new Map(); + +let helpersByRoot: Map = new Map(); // We keep track of mounted roots so we can schedule updates. let mountedRoots: Set = 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, ): Set { 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)) { diff --git a/packages/react-refresh/src/__tests__/ReactFreshMultipleRenderer-test.internal.js b/packages/react-refresh/src/__tests__/ReactFreshMultipleRenderer-test.internal.js new file mode 100644 index 0000000000000..2b260a5283597 --- /dev/null +++ b/packages/react-refresh/src/__tests__/ReactFreshMultipleRenderer-test.internal.js @@ -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 ; + }; + ReactFreshRuntime.register(InnerV1, 'Inner'); + + let OuterV1 = function() { + return ( +
+ + + +
+ ); + }; + ReactFreshRuntime.register(OuterV1, 'Outer'); + + ReactDOM.render(, 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 ; + }; + 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 ( +
+ + + +
+ ); + }; + 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)'); + } + }); +});