Skip to content

Commit

Permalink
[Fresh] Support multiple renderers at the same time (#16302)
Browse files Browse the repository at this point in the history
  • Loading branch information
gaearon committed Aug 8, 2019
1 parent 6f3c833 commit 12be893
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 66 deletions.
128 changes: 62 additions & 66 deletions packages/react-refresh/src/ReactFreshRuntime.js
Expand Up @@ -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,
Expand All @@ -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.',
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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.',
);

This comment has been minimized.

Copy link
@sebmarkbage

sebmarkbage Aug 9, 2019

Collaborator

Shouldn't this use invariant?

This comment has been minimized.

Copy link
@gaearon

gaearon Aug 9, 2019

Author Collaborator

I thought we just throw now. But then I remembered that never got merged?

Regardless this is mostly for Flow because this whole entry point throws outside DEV.

}
const instancesForRoot = helpers.findHostInstancesForRefresh(
root,
families,
);
instancesForRoot.forEach(inst => {
affectedInstances.add(inst);
});
Expand All @@ -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,
Expand All @@ -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;

Expand Down Expand Up @@ -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)) {
Expand Down
@@ -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)');
}
});
});

0 comments on commit 12be893

Please sign in to comment.