diff --git a/packages/react-refresh/package.json b/packages/react-refresh/package.json index 36b5102dc1e3e..84b3d704ee9d4 100644 --- a/packages/react-refresh/package.json +++ b/packages/react-refresh/package.json @@ -25,5 +25,9 @@ }, "engines": { "node": ">=0.10.0" + }, + "devDependencies": { + "react-16-8": "npm:react@16.8.0", + "react-dom-16-8": "npm:react-dom@16.8.0" } -} +} \ No newline at end of file diff --git a/packages/react-refresh/src/ReactFreshRuntime.js b/packages/react-refresh/src/ReactFreshRuntime.js index cd5d5a6716bf8..9cf8af21182b9 100644 --- a/packages/react-refresh/src/ReactFreshRuntime.js +++ b/packages/react-refresh/src/ReactFreshRuntime.js @@ -517,53 +517,53 @@ export function injectIntoGlobalHook(globalObject: any): void { didError: boolean, ) { const helpers = helpersByRendererID.get(id); - if (helpers === undefined) { - return; - } - helpersByRoot.set(root, helpers); - - const current = root.current; - const alternate = current.alternate; - - // We need to determine whether this root has just (un)mounted. - // This logic is copy-pasted from similar logic in the DevTools backend. - // If this breaks with some refactoring, you'll want to update DevTools too. - - if (alternate !== null) { - const wasMounted = - alternate.memoizedState != null && - alternate.memoizedState.element != null; - const isMounted = - current.memoizedState != null && - current.memoizedState.element != null; - - if (!wasMounted && isMounted) { + if (helpers !== undefined) { + helpersByRoot.set(root, helpers); + + const current = root.current; + const alternate = current.alternate; + + // We need to determine whether this root has just (un)mounted. + // This logic is copy-pasted from similar logic in the DevTools backend. + // If this breaks with some refactoring, you'll want to update DevTools too. + + if (alternate !== null) { + const wasMounted = + alternate.memoizedState != null && + alternate.memoizedState.element != null; + const isMounted = + current.memoizedState != null && + current.memoizedState.element != null; + + if (!wasMounted && isMounted) { + // Mount a new root. + mountedRoots.add(root); + failedRoots.delete(root); + } else if (wasMounted && isMounted) { + // Update an existing root. + // This doesn't affect our mounted root Set. + } else if (wasMounted && !isMounted) { + // Unmount an existing root. + mountedRoots.delete(root); + if (didError) { + // We'll remount it on future edits. + failedRoots.add(root); + } else { + helpersByRoot.delete(root); + } + } else if (!wasMounted && !isMounted) { + if (didError) { + // We'll remount it on future edits. + failedRoots.add(root); + } + } + } else { // Mount a new root. mountedRoots.add(root); - failedRoots.delete(root); - } else if (wasMounted && isMounted) { - // Update an existing root. - // This doesn't affect our mounted root Set. - } else if (wasMounted && !isMounted) { - // Unmount an existing root. - mountedRoots.delete(root); - if (didError) { - // We'll remount it on future edits. - failedRoots.add(root); - } else { - helpersByRoot.delete(root); - } - } else if (!wasMounted && !isMounted) { - if (didError) { - // We'll remount it on future edits. - failedRoots.add(root); - } } - } else { - // Mount a new root. - mountedRoots.add(root); } + // Always call the decorated DevTools hook. return oldOnCommitFiberRoot.apply(this, arguments); }; } else { diff --git a/packages/react-refresh/src/__tests__/ReactFresh-test.js b/packages/react-refresh/src/__tests__/ReactFresh-test.js index da73032520c64..5e4a766640ad2 100644 --- a/packages/react-refresh/src/__tests__/ReactFresh-test.js +++ b/packages/react-refresh/src/__tests__/ReactFresh-test.js @@ -3745,24 +3745,32 @@ describe('ReactFresh', () => { } }); - // This simulates the scenario in https://github.com/facebook/react/issues/17626. + function initFauxDevToolsHook() { + const onCommitFiberRoot = jest.fn(); + const onCommitFiberUnmount = jest.fn(); + + let idCounter = 0; + const renderers = new Map(); + + // This is a minimal shim for the global hook installed by DevTools. + // The real one is in packages/react-devtools-shared/src/hook.js. + global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = { + renderers, + supportsFiber: true, + inject(renderer) { + const id = ++idCounter; + renderers.set(id, renderer); + return id; + }, + onCommitFiberRoot, + onCommitFiberUnmount, + }; + } + + // This simulates the scenario in https://github.com/facebook/react/issues/17626 it('can inject the runtime after the renderer executes', () => { if (__DEV__) { - // This is a minimal shim for the global hook installed by DevTools. - // The real one is in packages/react-devtools-shared/src/hook.js. - let idCounter = 0; - const renderers = new Map(); - global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = { - renderers, - supportsFiber: true, - inject(renderer) { - const id = ++idCounter; - renderers.set(id, renderer); - return id; - }, - onCommitFiberRoot() {}, - onCommitFiberUnmount() {}, - }; + initFauxDevToolsHook(); // Load these first, as if they're coming from a CDN. jest.resetModules(); @@ -3820,4 +3828,39 @@ describe('ReactFresh', () => { expect(el.style.color).toBe('red'); } }); + + // This simulates the scenario in https://github.com/facebook/react/issues/20100 + it('does not block DevTools when an unsupported renderer is injected', () => { + if (__DEV__) { + initFauxDevToolsHook(); + + const onCommitFiberRoot = + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot; + + // Redirect all React/ReactDOM requires to v16.8.0 + // This version predates Fast Refresh support. + jest.mock('react', () => jest.requireActual('react-16-8')); + jest.mock('react-dom', () => jest.requireActual('react-dom-16-8')); + + // Load React and company. + jest.resetModules(); + React = require('react'); + ReactDOM = require('react-dom'); + Scheduler = require('scheduler'); + + // Important! Inject into the global hook *after* ReactDOM runs: + ReactFreshRuntime = require('react-refresh/runtime'); + ReactFreshRuntime.injectIntoGlobalHook(global); + + render(() => { + function Hello() { + return