Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 62 additions & 66 deletions packages/react-refresh/src/ReactFreshRuntime.js
Original file line number Diff line number Diff line change
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.',
);
}
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++;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not exactly what DevTools does but close enough.

},
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;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There can still be commits to renderers that don't support Fast Refresh. We ignore those.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be nice to leave as a comment.

}
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
Original file line number Diff line number Diff line change
@@ -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');
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are top level because I couldn't get ART to work otherwise. Dunno why.

Copy link
Contributor

@bvaughn bvaughn Aug 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that's unfortunate since it means you can't reset modules between tests (if you ever decide to add more than one 😆 )

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I spent 20 minutes on this and then flipped the table

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)');
}
});
});