-
Notifications
You must be signed in to change notification settings - Fork 47.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Fresh] Support multiple renderers at the same time (#16302)
- Loading branch information
Showing
2 changed files
with
161 additions
and
66 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<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.', | ||
|
@@ -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(); | ||
|
@@ -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<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.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
gaearon
Author
Collaborator
|
||
} | ||
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)) { | ||
|
99 changes: 99 additions & 0 deletions
99
packages/react-refresh/src/__tests__/ReactFreshMultipleRenderer-test.internal.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
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)'); | ||
} | ||
}); | ||
}); |
Shouldn't this use invariant?