-
Notifications
You must be signed in to change notification settings - Fork 50.5k
[Fresh] Support multiple renderers at the same time #16302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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.', | ||
| ); | ||
| } | ||
| 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; | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
|
||
|
|
@@ -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)) { | ||
|
|
||
| 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'); | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 😆 )
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)'); | ||
| } | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
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.