/*** Copyright (c) Meta Platforms, Inc. and 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*//* eslint-disable no-for-of-loops/no-for-of-loops */'use strict';
let React;
let ReactDOM;
let ReactDOMClient;
let ReactFreshRuntime;
let Scheduler;
let act;
let internalAct;
let createReactClass;
let waitFor;
let assertLog;
describe('ReactFresh', () => {
let container;
beforeEach(() => {
if (__DEV__) {
jest.resetModules();
React = require('react');
ReactFreshRuntime = require('react-refresh/runtime');
ReactFreshRuntime.injectIntoGlobalHook(global);
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
Scheduler = require('scheduler');
act = require('react-dom/test-utils').act;
internalAct = require('internal-test-utils').act;
const InternalTestUtils = require('internal-test-utils');
waitFor = InternalTestUtils.waitFor;
assertLog = InternalTestUtils.assertLog;
createReactClass = require('create-react-class/factory')(
React.Component,
React.isValidElement,
new React.Component().updater,
);
container = document.createElement('div');
document.body.appendChild(container);
}});afterEach(() => {
if (__DEV__) {
delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;document.body.removeChild(container);
}});function prepare(version) {
const Component = version();
return Component;
}function render(version, props) {
const Component = version();
act(() => {
ReactDOM.render(<Component {...props} />, container);
});return Component;
}function patch(version) {
const Component = version();
ReactFreshRuntime.performReactRefresh();
return Component;
}function $RefreshReg$(type, id) {
ReactFreshRuntime.register(type, id);
}function $RefreshSig$(type, key, forceReset, getCustomHooks) {
ReactFreshRuntime.setSignature(type, key, forceReset, getCustomHooks);
return type;
}// Note: This is based on a similar component we use in www. We can delete
// once the extra div wrapper is no longer necessary.
function LegacyHiddenDiv({children, mode}) {
return (
<div hidden={mode === 'hidden'}>
<React.unstable_LegacyHidden
mode={mode === 'hidden' ? 'unstable-defer-without-hiding' : mode}>
{children}
</React.unstable_LegacyHidden>
</div>
);
}it('can preserve state for compatible types', () => {
if (__DEV__) {
const HelloV1 = render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
return Hello;});// Bump the state before patching.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('1');
// Perform a hot update.
const HelloV2 = patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
return Hello;});// Assert the state was preserved but color changed.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
// Bump the state again.
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
// Perform top-down renders with both fresh and stale types.
// Neither should change the state or color.
// They should always resolve to the latest version.
render(() => HelloV1);
render(() => HelloV2);
render(() => HelloV1);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
// Bump the state again.
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('3');
expect(el.style.color).toBe('red');
// Finally, a render with incompatible type should reset it.
render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}// No register call.
// This is considered a new type.
return Hello;
});expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('0');
expect(newEl.style.color).toBe('blue');
}});it('can preserve state for forwardRef', () => {
if (__DEV__) {
const OuterV1 = render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
const Outer = React.forwardRef(() => <Hello />);
$RefreshReg$(Outer, 'Outer');
return Outer;});// Bump the state before patching.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('1');
// Perform a hot update.
const OuterV2 = patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
const Outer = React.forwardRef(() => <Hello />);
$RefreshReg$(Outer, 'Outer');
return Outer;});// Assert the state was preserved but color changed.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
// Bump the state again.
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
// Perform top-down renders with both fresh and stale types.
// Neither should change the state or color.
// They should always resolve to the latest version.
render(() => OuterV1);
render(() => OuterV2);
render(() => OuterV1);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
// Finally, a render with incompatible type should reset it.
render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
// Note: no forwardRef wrapper this time.
return Hello;
});expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('0');
expect(newEl.style.color).toBe('blue');
}});it('should not consider two forwardRefs around the same type to be equivalent', () => {
if (__DEV__) {
const ParentV1 = render(
() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
function renderInner() {return <Hello />;
}// Both of these are wrappers around the same inner function.
// They should be treated as distinct types across reloads.
const ForwardRefA = React.forwardRef(renderInner);
$RefreshReg$(ForwardRefA, 'ForwardRefA');
const ForwardRefB = React.forwardRef(renderInner);
$RefreshReg$(ForwardRefB, 'ForwardRefB');
function Parent({cond}) {
return cond ? <ForwardRefA /> : <ForwardRefB />;
}$RefreshReg$(Parent, 'Parent');
return Parent;
},{cond: true},
);
// Bump the state before switching up types.
let el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('1');
// Switching up the inner types should reset the state.
render(() => ParentV1, {cond: false});
expect(el).not.toBe(container.firstChild);
el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('1');
// Switch them up back again.
render(() => ParentV1, {cond: true});
expect(el).not.toBe(container.firstChild);
el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
// Now bump up the state to prepare for patching.
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('1');
// Patch to change the color.
const ParentV2 = patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
function renderInner() {return <Hello />;
}// Both of these are wrappers around the same inner function.
// They should be treated as distinct types across reloads.
const ForwardRefA = React.forwardRef(renderInner);
$RefreshReg$(ForwardRefA, 'ForwardRefA');
const ForwardRefB = React.forwardRef(renderInner);
$RefreshReg$(ForwardRefB, 'ForwardRefB');
function Parent({cond}) {
return cond ? <ForwardRefA /> : <ForwardRefB />;
}$RefreshReg$(Parent, 'Parent');
return Parent;
});// The state should be intact; the color should change.
expect(el).toBe(container.firstChild);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
// Switching up the condition should still reset the state.
render(() => ParentV2, {cond: false});
expect(el).not.toBe(container.firstChild);
el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('red');
// Now bump up the state to prepare for top-level renders.
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el).toBe(container.firstChild);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
// Finally, verify using top-level render with stale type keeps state.
render(() => ParentV1);
render(() => ParentV2);
render(() => ParentV1);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
}});it('can update forwardRef render function with its wrapper', () => {
if (__DEV__) {
render(() => {
function Hello({color}) {
const [val, setVal] = React.useState(0);
return (
<p style={{color}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
const Outer = React.forwardRef(() => <Hello color="blue" />);
$RefreshReg$(Outer, 'Outer');
return Outer;});// Bump the state before patching.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('1');
// Perform a hot update.
patch(() => {
function Hello({color}) {
const [val, setVal] = React.useState(0);
return (
<p style={{color}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
const Outer = React.forwardRef(() => <Hello color="red" />);
$RefreshReg$(Outer, 'Outer');
return Outer;});// Assert the state was preserved but color changed.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
}});it('can update forwardRef render function in isolation', () => {
if (__DEV__) {
render(() => {
function Hello({color}) {
const [val, setVal] = React.useState(0);
return (
<p style={{color}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
function renderHello() {return <Hello color="blue" />;
}$RefreshReg$(renderHello, 'renderHello');
return React.forwardRef(renderHello);
});// Bump the state before patching.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('1');
// Perform a hot update of just the rendering function.
patch(() => {
function Hello({color}) {
const [val, setVal] = React.useState(0);
return (
<p style={{color}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
function renderHello() {return <Hello color="red" />;
}$RefreshReg$(renderHello, 'renderHello');
// Not updating the wrapper.
});// Assert the state was preserved but color changed.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
}});it('can preserve state for simple memo', () => {
if (__DEV__) {
const OuterV1 = render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
const Outer = React.memo(Hello);
$RefreshReg$(Outer, 'Outer');
return Outer;});// Bump the state before patching.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('1');
// Perform a hot update.
const OuterV2 = patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
const Outer = React.memo(Hello);
$RefreshReg$(Outer, 'Outer');
return Outer;});// Assert the state was preserved but color changed.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
// Bump the state again.
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
// Perform top-down renders with both fresh and stale types.
// Neither should change the state or color.
// They should always resolve to the latest version.
render(() => OuterV1);
render(() => OuterV2);
render(() => OuterV1);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
// Finally, a render with incompatible type should reset it.
render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
// Note: no wrapper this time.
return Hello;
});expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('0');
expect(newEl.style.color).toBe('blue');
}});it('can preserve state for memo with custom comparison', () => {
if (__DEV__) {
const OuterV1 = render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}const Outer = React.memo(Hello, () => true);
$RefreshReg$(Outer, 'Outer');
return Outer;});// Bump the state before patching.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('1');
// Perform a hot update.
const OuterV2 = patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}const Outer = React.memo(Hello, () => true);
$RefreshReg$(Outer, 'Outer');
return Outer;});// Assert the state was preserved but color changed.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
// Bump the state again.
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
// Perform top-down renders with both fresh and stale types.
// Neither should change the state or color.
// They should always resolve to the latest version.
render(() => OuterV1);
render(() => OuterV2);
render(() => OuterV1);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
// Finally, a render with incompatible type should reset it.
render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
// Note: no wrapper this time.
return Hello;
});expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('0');
expect(newEl.style.color).toBe('blue');
}});it('can update simple memo function in isolation', () => {
if (__DEV__) {
render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
return React.memo(Hello);
});// Bump the state before patching.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('1');
// Perform a hot update of just the rendering function.
patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
// Not updating the wrapper.
});// Assert the state was preserved but color changed.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
}});it('can preserve state for memo(forwardRef)', () => {
if (__DEV__) {
const OuterV1 = render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
const Outer = React.memo(React.forwardRef(() => <Hello />));
$RefreshReg$(Outer, 'Outer');
return Outer;});// Bump the state before patching.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('1');
// Perform a hot update.
const OuterV2 = patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
const Outer = React.memo(React.forwardRef(() => <Hello />));
$RefreshReg$(Outer, 'Outer');
return Outer;});// Assert the state was preserved but color changed.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
// Bump the state again.
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
// Perform top-down renders with both fresh and stale types.
// Neither should change the state or color.
// They should always resolve to the latest version.
render(() => OuterV1);
render(() => OuterV2);
render(() => OuterV1);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
// Finally, a render with incompatible type should reset it.
render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
// Note: no wrapper this time.
return Hello;
});expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('0');
expect(newEl.style.color).toBe('blue');
}});it('can preserve state for lazy after resolution', async () => {
if (__DEV__) {
const AppV1 = render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
const Outer = React.lazy(
() =>new Promise(resolve => {setTimeout(() => resolve({default: Hello}), 100);}),);$RefreshReg$(Outer, 'Outer');
function App() {return (<React.Suspense fallback={<p>Loading</p>}>
<Outer /></React.Suspense>
);}$RefreshReg$(App, 'App');
return App;});expect(container.textContent).toBe('Loading');
await act(() => {jest.runAllTimers();
});expect(container.textContent).toBe('0');
// Bump the state before patching.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('1');
// Perform a hot update.
const AppV2 = patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
const Outer = React.lazy(
() =>new Promise(resolve => {setTimeout(() => resolve({default: Hello}), 100);}),);$RefreshReg$(Outer, 'Outer');
function App() {return (<React.Suspense fallback={<p>Loading</p>}>
<Outer /></React.Suspense>
);}$RefreshReg$(App, 'App');
return App;});// Assert the state was preserved but color changed.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
// Bump the state again.
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
// Perform top-down renders with both fresh and stale types.
// Neither should change the state or color.
// They should always resolve to the latest version.
render(() => AppV1);
render(() => AppV2);
render(() => AppV1);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
// Finally, a render with incompatible type should reset it.
render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
// Note: no lazy wrapper this time.
function App() {
return (
<React.Suspense fallback={<p>Loading</p>}>
<Hello />
</React.Suspense>
);}$RefreshReg$(App, 'App');
return App;});expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('0');
expect(newEl.style.color).toBe('blue');
}});it('can patch lazy before resolution', async () => {if (__DEV__) {render(() => {function Hello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}</p>);}$RefreshReg$(Hello, 'Hello');
const Outer = React.lazy(
() =>new Promise(resolve => {setTimeout(() => resolve({default: Hello}), 100);}),);$RefreshReg$(Outer, 'Outer');
function App() {return (<React.Suspense fallback={<p>Loading</p>}>
<Outer /></React.Suspense>
);}return App;});expect(container.textContent).toBe('Loading');
// Perform a hot update.
patch(() => {function Hello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}</p>);}$RefreshReg$(Hello, 'Hello');
});await act(() => {jest.runAllTimers();
});// Expect different color on initial mount.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('red');
// Bump state.
act(() => {el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
// Test another reload.
patch(() => {function Hello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{val}</p>);}$RefreshReg$(Hello, 'Hello');
});expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('orange');
}});it('can patch lazy(forwardRef) before resolution', async () => {if (__DEV__) {render(() => {function renderHello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}</p>);}const Hello = React.forwardRef(renderHello);
$RefreshReg$(Hello, 'Hello');
const Outer = React.lazy(
() =>new Promise(resolve => {setTimeout(() => resolve({default: Hello}), 100);}),);$RefreshReg$(Outer, 'Outer');
function App() {return (<React.Suspense fallback={<p>Loading</p>}>
<Outer /></React.Suspense>
);}return App;});expect(container.textContent).toBe('Loading');
// Perform a hot update.
patch(() => {function renderHello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}</p>);}const Hello = React.forwardRef(renderHello);
$RefreshReg$(Hello, 'Hello');
});await act(() => {jest.runAllTimers();
});// Expect different color on initial mount.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('red');
// Bump state.
act(() => {el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
// Test another reload.
patch(() => {function renderHello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{val}</p>);}const Hello = React.forwardRef(renderHello);
$RefreshReg$(Hello, 'Hello');
});expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('orange');
}});it('can patch lazy(memo) before resolution', async () => {if (__DEV__) {render(() => {function renderHello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}</p>);}const Hello = React.memo(renderHello);
$RefreshReg$(Hello, 'Hello');
const Outer = React.lazy(
() =>new Promise(resolve => {setTimeout(() => resolve({default: Hello}), 100);}),);$RefreshReg$(Outer, 'Outer');
function App() {return (<React.Suspense fallback={<p>Loading</p>}>
<Outer /></React.Suspense>
);}return App;});expect(container.textContent).toBe('Loading');
// Perform a hot update.
patch(() => {function renderHello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}</p>);}const Hello = React.memo(renderHello);
$RefreshReg$(Hello, 'Hello');
});await act(() => {jest.runAllTimers();
});// Expect different color on initial mount.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('red');
// Bump state.
act(() => {el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
// Test another reload.
patch(() => {function renderHello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{val}</p>);}const Hello = React.memo(renderHello);
$RefreshReg$(Hello, 'Hello');
});expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('orange');
}});it('can patch lazy(memo(forwardRef)) before resolution', async () => {if (__DEV__) {render(() => {function renderHello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}</p>);}const Hello = React.memo(React.forwardRef(renderHello));
$RefreshReg$(Hello, 'Hello');
const Outer = React.lazy(
() =>new Promise(resolve => {setTimeout(() => resolve({default: Hello}), 100);}),);$RefreshReg$(Outer, 'Outer');
function App() {return (<React.Suspense fallback={<p>Loading</p>}>
<Outer /></React.Suspense>
);}return App;});expect(container.textContent).toBe('Loading');
// Perform a hot update.
patch(() => {function renderHello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}</p>);}const Hello = React.memo(React.forwardRef(renderHello));
$RefreshReg$(Hello, 'Hello');
});await act(() => {jest.runAllTimers();
});// Expect different color on initial mount.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('red');
// Bump state.
act(() => {el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
// Test another reload.
patch(() => {function renderHello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{val}</p>);}const Hello = React.memo(React.forwardRef(renderHello));
$RefreshReg$(Hello, 'Hello');
});expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('orange');
}});it('can patch both trees while suspense is displaying the fallback', async () => {if (__DEV__) {const AppV1 = render(() => {function Hello({children}) {const [val, setVal] = React.useState(0);
return (<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{children} {val}</p>);}$RefreshReg$(Hello, 'Hello');
function Never() {throw new Promise(resolve => {});}function App({shouldSuspend}) {return (<React.Suspense fallback={<Hello>Fallback</Hello>}>
<Hello>Content</Hello>{shouldSuspend && <Never />}</React.Suspense>
);}return App;},{shouldSuspend: false},);// We start with just the primary tree.
expect(container.childNodes.length).toBe(1);
const primaryChild = container.firstChild;
expect(primaryChild.textContent).toBe('Content 0');
expect(primaryChild.style.color).toBe('blue');
expect(primaryChild.style.display).toBe('');
// Bump primary content state.
act(() => {primaryChild.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(container.childNodes.length).toBe(1);
expect(container.childNodes[0]).toBe(primaryChild);
expect(primaryChild.textContent).toBe('Content 1');
expect(primaryChild.style.color).toBe('blue');
expect(primaryChild.style.display).toBe('');
// Perform a hot update.
patch(() => {function Hello({children}) {const [val, setVal] = React.useState(0);
return (<p style={{color: 'green'}} onClick={() => setVal(val + 1)}>
{children} {val}</p>);}$RefreshReg$(Hello, 'Hello');
});expect(container.childNodes.length).toBe(1);
expect(container.childNodes[0]).toBe(primaryChild);
expect(primaryChild.textContent).toBe('Content 1');
expect(primaryChild.style.color).toBe('green');
expect(primaryChild.style.display).toBe('');
// Now force the tree to suspend.
render(() => AppV1, {shouldSuspend: true});// Expect to see two trees, one of them is hidden.
expect(container.childNodes.length).toBe(2);
expect(container.childNodes[0]).toBe(primaryChild);
const fallbackChild = container.childNodes[1];
expect(primaryChild.textContent).toBe('Content 1');
expect(primaryChild.style.color).toBe('green');
expect(primaryChild.style.display).toBe('none');
expect(fallbackChild.textContent).toBe('Fallback 0');
expect(fallbackChild.style.color).toBe('green');
expect(fallbackChild.style.display).toBe('');
// Bump fallback state.
act(() => {fallbackChild.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(container.childNodes.length).toBe(2);
expect(container.childNodes[0]).toBe(primaryChild);
expect(container.childNodes[1]).toBe(fallbackChild);
expect(primaryChild.textContent).toBe('Content 1');
expect(primaryChild.style.color).toBe('green');
expect(primaryChild.style.display).toBe('none');
expect(fallbackChild.textContent).toBe('Fallback 1');
expect(fallbackChild.style.color).toBe('green');
expect(fallbackChild.style.display).toBe('');
// Perform a hot update.
patch(() => {function Hello({children}) {const [val, setVal] = React.useState(0);
return (<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{children} {val}</p>);}$RefreshReg$(Hello, 'Hello');
});// Colors inside both trees should change:expect(container.childNodes.length).toBe(2);
expect(container.childNodes[0]).toBe(primaryChild);
expect(container.childNodes[1]).toBe(fallbackChild);
expect(primaryChild.textContent).toBe('Content 1');
expect(primaryChild.style.color).toBe('red');
expect(primaryChild.style.display).toBe('none');
expect(fallbackChild.textContent).toBe('Fallback 1');
expect(fallbackChild.style.color).toBe('red');
expect(fallbackChild.style.display).toBe('');
// Only primary tree should exist now:render(() => AppV1, {shouldSuspend: false});expect(container.childNodes.length).toBe(1);
expect(container.childNodes[0]).toBe(primaryChild);
expect(primaryChild.textContent).toBe('Content 1');
expect(primaryChild.style.color).toBe('red');
expect(primaryChild.style.display).toBe('');
// Perform a hot update.
patch(() => {function Hello({children}) {const [val, setVal] = React.useState(0);
return (<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{children} {val}</p>);}$RefreshReg$(Hello, 'Hello');
});expect(container.childNodes.length).toBe(1);
expect(container.childNodes[0]).toBe(primaryChild);
expect(primaryChild.textContent).toBe('Content 1');
expect(primaryChild.style.color).toBe('orange');
expect(primaryChild.style.display).toBe('');
}});it('does not re-render ancestor components unnecessarily during a hot update', () => {if (__DEV__) {let appRenders = 0;render(() => {function Hello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}</p>);}$RefreshReg$(Hello, 'Hello');
function App() {appRenders++;
return <Hello />;}$RefreshReg$(App, 'App');
return App;});expect(appRenders).toBe(1);
// Bump the state before patching.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('1');
// No re-renders from the top.
expect(appRenders).toBe(1);
// Perform a hot update for Hello only.
patch(() => {function Hello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}</p>);}$RefreshReg$(Hello, 'Hello');
});// Assert the state was preserved but color changed.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
// Still no re-renders from the top.
expect(appRenders).toBe(1);
// Bump the state.
act(() => {el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('2');
// Still no re-renders from the top.
expect(appRenders).toBe(1);
}});it('batches re-renders during a hot update', () => {if (__DEV__) {let helloRenders = 0;render(() => {function Hello({children}) {helloRenders++;
return <div>X{children}X</div>;}$RefreshReg$(Hello, 'Hello');
function App() {return (<Hello><Hello><Hello /></Hello><Hello><Hello /></Hello></Hello>);}return App;});expect(helloRenders).toBe(5);
expect(container.textContent).toBe('XXXXXXXXXX');
helloRenders = 0;patch(() => {function Hello({children}) {helloRenders++;
return <div>O{children}O</div>;}$RefreshReg$(Hello, 'Hello');
});expect(helloRenders).toBe(5);
expect(container.textContent).toBe('OOOOOOOOOO');
}});it('does not leak state between components', () => {if (__DEV__) {const AppV1 = render(() => {function Hello1() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}</p>);}$RefreshReg$(Hello1, 'Hello1');
function Hello2() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}</p>);}$RefreshReg$(Hello2, 'Hello2');
function App({cond}) {return cond ? <Hello1 /> : <Hello2 />;
}$RefreshReg$(App, 'App');
return App;},{cond: false},);// Bump the state before patching.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('1');
// Switch the condition, flipping inner content.
// This should reset the state.
render(() => AppV1, {cond: true});const el2 = container.firstChild;
expect(el2).not.toBe(el);
expect(el2.textContent).toBe('0');
expect(el2.style.color).toBe('blue');
// Bump it again.
act(() => {el2.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el2.textContent).toBe('1');
// Perform a hot update for both inner components.
patch(() => {function Hello1() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}</p>);}$RefreshReg$(Hello1, 'Hello1');
function Hello2() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}</p>);}$RefreshReg$(Hello2, 'Hello2');
});// Assert the state was preserved but color changed.
expect(container.firstChild).toBe(el2);
expect(el2.textContent).toBe('1');
expect(el2.style.color).toBe('red');
// Flip the condition again.
render(() => AppV1, {cond: false});const el3 = container.firstChild;
expect(el3).not.toBe(el2);
expect(el3.textContent).toBe('0');
expect(el3.style.color).toBe('red');
}});it('can force remount by changing signature', () => {if (__DEV__) {const HelloV1 = render(() => {function Hello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}</p>);}$RefreshReg$(Hello, 'Hello');
// When this changes, we'll expect a remount:$RefreshSig$(Hello, '1');
return Hello;});// Bump the state before patching.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('1');
// Perform a hot update.
const HelloV2 = patch(() => {function Hello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}</p>);}$RefreshReg$(Hello, 'Hello');
// The signature hasn't changed since the last time:$RefreshSig$(Hello, '1');
return Hello;});// Assert the state was preserved but color changed.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
// Perform a hot update.
const HelloV3 = patch(() => {function Hello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'yellow'}} onClick={() => setVal(val + 1)}>
{val}</p>);}// We're changing the signature now so it will remount:$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '2');
return Hello;});// Expect a remount.
expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('0');
expect(newEl.style.color).toBe('yellow');
// Bump state again.
act(() => {newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(newEl.textContent).toBe('1');
expect(newEl.style.color).toBe('yellow');
// Perform top-down renders with both fresh and stale types.
// Neither should change the state or color.
// They should always resolve to the latest version.
render(() => HelloV1);render(() => HelloV2);render(() => HelloV3);render(() => HelloV2);render(() => HelloV1);expect(container.firstChild).toBe(newEl);
expect(newEl.textContent).toBe('1');
expect(newEl.style.color).toBe('yellow');
// Verify we can patch again while preserving the signature.
patch(() => {function Hello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'purple'}} onClick={() => setVal(val + 1)}>
{val}</p>);}// Same signature as last time.
$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '2');
return Hello;});expect(container.firstChild).toBe(newEl);
expect(newEl.textContent).toBe('1');
expect(newEl.style.color).toBe('purple');
// Check removing the signature also causes a remount.
patch(() => {function Hello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{val}</p>);}// No signature this time.
$RefreshReg$(Hello, 'Hello');
return Hello;});// Expect a remount.
expect(container.firstChild).not.toBe(newEl);
const finalEl = container.firstChild;
expect(finalEl.textContent).toBe('0');
expect(finalEl.style.color).toBe('orange');
}});it('keeps a valid tree when forcing remount', () => {if (__DEV__) {const HelloV1 = prepare(() => {function Hello() {return null;}$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '1');
return Hello;});const Bailout = React.memo(({children}) => {
return children;});// Each of those renders three instances of HelloV1,// but in different ways.
const trees = [
<div><HelloV1 /><div><HelloV1 /><Bailout><HelloV1 /></Bailout></div></div>,<div><div><HelloV1><HelloV1 /></HelloV1><HelloV1 /></div></div>,<div><span /><HelloV1 /><HelloV1 /><HelloV1 /></div>,<div><HelloV1 /><span /><HelloV1 /><HelloV1 /></div>,<div><div>foo</div><HelloV1 /><div><HelloV1 /></div><HelloV1 /><span /></div>,<div><HelloV1><span />Hello<span /></HelloV1>,<HelloV1><><HelloV1 /></></HelloV1>,</div>,<HelloV1><HelloV1><Bailout><span /><HelloV1><span /></HelloV1><span /></Bailout></HelloV1></HelloV1>,<div><span /><HelloV1 key="0" /><HelloV1 key="1" /><HelloV1 key="2" /><span /></div>,<div><span />{null}<HelloV1 key="1" />{null}<HelloV1 /><HelloV1 /><span /></div>,<div><HelloV1 key="2" /><span /><HelloV1 key="0" /><span /><HelloV1 key="1" /></div>,<div>{[[<HelloV1 key="2" />]]}
<span><HelloV1 key="0" />{[null]}
<HelloV1 key="1" /></span></div>,<div>{['foo', <HelloV1 key="hi" />, null, <HelloV1 key="2" />]}
<span>{[null]}
<HelloV1 key="x" /></span></div>,<HelloV1><HelloV1><span /><Bailout><HelloV1>hi</HelloV1><span /></Bailout></HelloV1></HelloV1>,];// First, check that each tree handles remounts in isolation.
ReactDOM.render(null, container);
for (let i = 0; i < trees.length; i++) {
runRemountingStressTest(trees[i]);
}// Then check that each tree is resilient to updates from another tree.
for (let i = 0; i < trees.length; i++) {
for (let j = 0; j < trees.length; j++) {
ReactDOM.render(null, container);
// Intentionally don't clean up between the tests:runRemountingStressTest(trees[i]);
runRemountingStressTest(trees[j]);
runRemountingStressTest(trees[i]);
}}}});function runRemountingStressTest(tree) {patch(() => {function Hello({children}) {return <section data-color="blue">{children}</section>;}$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '1');
return Hello;});ReactDOM.render(tree, container);
const elements = container.querySelectorAll('section');
// Each tree above produces exactly three <section> elements:
expect(elements.length).toBe(3);
elements.forEach(el => {
expect(el.dataset.color).toBe('blue');
});// Patch color without changing the signature.
patch(() => {
function Hello({children}) {
return <section data-color="red">{children}</section>;
}$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '1');
return Hello;});const elementsAfterPatch = container.querySelectorAll('section');
expect(elementsAfterPatch.length).toBe(3);
elementsAfterPatch.forEach((el, index) => {
// The signature hasn't changed so we expect DOM nodes to stay the same.
expect(el).toBe(elements[index]);
// However, the color should have changed:expect(el.dataset.color).toBe('red');
});// Patch color *and* change the signature.
patch(() => {
function Hello({children}) {
return <section data-color="orange">{children}</section>;
}$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '2'); // Remount
return Hello;
});
const elementsAfterRemount = container.querySelectorAll('section');
expect(elementsAfterRemount.length).toBe(3);
elementsAfterRemount.forEach((el, index) => {
// The signature changed so we expect DOM nodes to be different.
expect(el).not.toBe(elements[index]);
// They should all be using the new color:
expect(el.dataset.color).toBe('orange');
});// Now patch color but *don't* change the signature.
patch(() => {
function Hello({children}) {
return <section data-color="black">{children}</section>;
}$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '2'); // Same signature as before
return Hello;
});
expect(container.querySelectorAll('section').length).toBe(3);
container.querySelectorAll('section').forEach((el, index) => {
// The signature didn't change so DOM nodes should stay the same.
expect(el).toBe(elementsAfterRemount[index]);
// They should all be using the new color:
expect(el.dataset.color).toBe('black');
});// Do another render just in case.
ReactDOM.render(tree, container);
expect(container.querySelectorAll('section').length).toBe(3);
container.querySelectorAll('section').forEach((el, index) => {
expect(el).toBe(elementsAfterRemount[index]);
expect(el.dataset.color).toBe('black');
});}it('can remount on signature change within a <root> wrapper', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => Hello);
}});it('can remount on signature change within a simple memo wrapper', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => React.memo(Hello));
}});it('can remount on signature change within a lazy simple memo wrapper', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello =>
React.lazy(() => ({
then(cb) {
cb({default: React.memo(Hello)});
},})),);}});it('can remount on signature change within forwardRef', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => React.forwardRef(Hello));
}});it('can remount on signature change within forwardRef render function', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => React.forwardRef(() => <Hello />));
}});it('can remount on signature change within nested memo', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello =>
React.memo(React.memo(React.memo(Hello))),
);}});it('can remount on signature change within a memo wrapper and custom comparison', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => React.memo(Hello, () => true));
}});it('can remount on signature change within a class', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => {
const child = <Hello />;
return class Wrapper extends React.PureComponent {
render() {
return child;
}};
});}});it('can remount on signature change within a context provider', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => {
const Context = React.createContext();
const child = (
<Context.Provider value="constant">
<Hello />
</Context.Provider>
);return function Wrapper() {return child;};});}});it('can remount on signature change within a context consumer', () => {if (__DEV__) {testRemountingWithWrapper(Hello => {const Context = React.createContext();
const child = <Context.Consumer>{() => <Hello />}</Context.Consumer>;
return function Wrapper() {return child;};});}});it('can remount on signature change within a suspense node', () => {if (__DEV__) {testRemountingWithWrapper(Hello => {// TODO: we'll probably want to test fallback trees too.
const child = (<React.Suspense>
<Hello /></React.Suspense>
);return function Wrapper() {return child;};});}});it('can remount on signature change within a mode node', () => {if (__DEV__) {testRemountingWithWrapper(Hello => {const child = (<React.StrictMode>
<Hello /></React.StrictMode>
);return function Wrapper() {return child;};});}});it('can remount on signature change within a fragment node', () => {if (__DEV__) {testRemountingWithWrapper(Hello => {const child = (<><Hello /></>);return function Wrapper() {return child;};});}});it('can remount on signature change within multiple siblings', () => {if (__DEV__) {testRemountingWithWrapper(Hello => {const child = (<><><React.Fragment />
</><Hello /><React.Fragment />
</>);return function Wrapper() {return child;};});}});it('can remount on signature change within a profiler node', () => {if (__DEV__) {testRemountingWithWrapper(Hello => {const child = <Hello />;return function Wrapper() {return (<React.Profiler onRender={() => {}} id="foo">
{child}</React.Profiler>
);};});}});function testRemountingWithWrapper(wrap) {render(() => {function Hello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}</p>);}$RefreshReg$(Hello, 'Hello');
// When this changes, we'll expect a remount:$RefreshSig$(Hello, '1');
// Use the passed wrapper.
// This will be different in every test.
return wrap(Hello);});// Bump the state before patching.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('1');
// Perform a hot update that doesn't remount.
patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
// The signature hasn't changed since the last time:
$RefreshSig$(Hello, '1');
return Hello;
});// Assert the state was preserved but color changed.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
// Perform a hot update that remounts.
patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'yellow'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}// We're changing the signature now so it will remount:
$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '2');
return Hello;
});// Expect a remount.
expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('0');
expect(newEl.style.color).toBe('yellow');
// Bump state again.
act(() => {
newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(newEl.textContent).toBe('1');
expect(newEl.style.color).toBe('yellow');
// Verify we can patch again while preserving the signature.
patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'purple'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}// Same signature as last time.
$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '2');
return Hello;
});expect(container.firstChild).toBe(newEl);
expect(newEl.textContent).toBe('1');
expect(newEl.style.color).toBe('purple');
// Check removing the signature also causes a remount.
patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}// No signature this time.
$RefreshReg$(Hello, 'Hello');
return Hello;
});// Expect a remount.
expect(container.firstChild).not.toBe(newEl);
const finalEl = container.firstChild;
expect(finalEl.textContent).toBe('0');
expect(finalEl.style.color).toBe('orange');
}it('resets hooks with dependencies on hot reload', () => {
if (__DEV__) {
let useEffectWithEmptyArrayCalls = 0;
render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
const tranformed = React.useMemo(() => val * 2, [val]);
const handleClick = React.useCallback(() => setVal(v => v + 1), []);
React.useEffect(() => {
useEffectWithEmptyArrayCalls++;
}, []);return (
<p style={{color: 'blue'}} onClick={handleClick}>
{tranformed}
</p>
);}$RefreshReg$(Hello, 'Hello');
return Hello;});// Bump the state before patching.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
expect(useEffectWithEmptyArrayCalls).toBe(1); // useEffect ran
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('2'); // val * 2
expect(useEffectWithEmptyArrayCalls).toBe(1); // useEffect didn't re-run
// Perform a hot update.
act(() => {
patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
const tranformed = React.useMemo(() => val * 10, [val]);
const handleClick = React.useCallback(() => setVal(v => v - 1), []);
React.useEffect(() => {
useEffectWithEmptyArrayCalls++;
}, []);return (
<p style={{color: 'red'}} onClick={handleClick}>
{tranformed}
</p>
);}$RefreshReg$(Hello, 'Hello');
return Hello;});});// Assert the state was preserved but memo was evicted.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('10'); // val * 10
expect(el.style.color).toBe('red');
expect(useEffectWithEmptyArrayCalls).toBe(2); // useEffect re-ran
// This should fire the new callback which decreases the counter.
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('0');
expect(el.style.color).toBe('red');
expect(useEffectWithEmptyArrayCalls).toBe(2); // useEffect didn't re-run
}});// This pattern is inspired by useSubscription and similar mechanisms.
it('does not get into infinite loops during render phase updates', () => {
if (__DEV__) {
render(() => {
function Hello() {
const source = React.useMemo(() => ({value: 10}), []);
const [state, setState] = React.useState({value: null});
if (state !== source) {
setState(source);
}return <p style={{color: 'blue'}}>{state.value}</p>;
}$RefreshReg$(Hello, 'Hello');
return Hello;});const el = container.firstChild;
expect(el.textContent).toBe('10');
expect(el.style.color).toBe('blue');
// Perform a hot update.
act(() => {
patch(() => {
function Hello() {
const source = React.useMemo(() => ({value: 20}), []);
const [state, setState] = React.useState({value: null});
if (state !== source) {
// This should perform a single render-phase update.
setState(source);
}return <p style={{color: 'red'}}>{state.value}</p>;
}$RefreshReg$(Hello, 'Hello');
return Hello;});});expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('20');
expect(el.style.color).toBe('red');
}});// @gate www && __DEV__
it('can hot reload offscreen components', async () => {
const AppV1 = prepare(() => {
function Hello() {
React.useLayoutEffect(() => {
Scheduler.log('Hello#layout');
});const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
return function App({offscreen}) {React.useLayoutEffect(() => {
Scheduler.log('App#layout');
});return (<LegacyHiddenDiv mode={offscreen ? 'hidden' : 'visible'}>
<Hello /></LegacyHiddenDiv>);};});const root = ReactDOMClient.createRoot(container);
root.render(<AppV1 offscreen={true} />);
await waitFor(['App#layout']);
const el = container.firstChild;
expect(el.hidden).toBe(true);
expect(el.firstChild).toBe(null); // Offscreen content not flushed yet.
// Perform a hot update.
patch(() => {
function Hello() {
React.useLayoutEffect(() => {
Scheduler.log('Hello#layout');
});const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
});// It's still offscreen so we don't see anything.
expect(container.firstChild).toBe(el);
expect(el.hidden).toBe(true);
expect(el.firstChild).toBe(null);
// Process the offscreen updates.
await waitFor(['Hello#layout']);
expect(container.firstChild).toBe(el);
expect(el.firstChild.textContent).toBe('0');
expect(el.firstChild.style.color).toBe('red');
await internalAct(() => {
el.firstChild.dispatchEvent(
new MouseEvent('click', {
bubbles: true,
}),);});assertLog(['Hello#layout']);
expect(el.firstChild.textContent).toBe('1');
expect(el.firstChild.style.color).toBe('red');
// Hot reload while we're offscreen.
patch(() => {
function Hello() {
React.useLayoutEffect(() => {
Scheduler.log('Hello#layout');
});const [val, setVal] = React.useState(0);
return (
<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
});// It's still offscreen so we don't see the updates.
expect(container.firstChild).toBe(el);
expect(el.firstChild.textContent).toBe('1');
expect(el.firstChild.style.color).toBe('red');
// Process the offscreen updates.
await waitFor(['Hello#layout']);
expect(container.firstChild).toBe(el);
expect(el.firstChild.textContent).toBe('1');
expect(el.firstChild.style.color).toBe('orange');
});it('remounts failed error boundaries (componentDidCatch)', () => {
if (__DEV__) {
render(() => {
function Hello() {
return <h1>Hi</h1>;
}$RefreshReg$(Hello, 'Hello');
class Boundary extends React.Component {
state = {error: null};componentDidCatch(error) {this.setState({error});
}render() {if (this.state.error) {
return <h1>Oops: {this.state.error.message}</h1>;
}return this.props.children;
}}function App() {
return (
<><p>A</p>
<Boundary><Hello />
</Boundary>
<p>B</p>
</>
);}return App;});expect(container.innerHTML).toBe('<p>A</p><h1>Hi</h1><p>B</p>');
const firstP = container.firstChild;
const secondP = firstP.nextSibling.nextSibling;
// Perform a hot update that fails.
patch(() => {
function Hello() {
throw new Error('No');
}$RefreshReg$(Hello, 'Hello');
});expect(container.innerHTML).toBe('<p>A</p><h1>Oops: No</h1><p>B</p>');
expect(container.firstChild).toBe(firstP);
expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
// Perform a hot update that fixes the error.
patch(() => {
function Hello() {
return <h1>Fixed!</h1>;
}$RefreshReg$(Hello, 'Hello');
});// This should remount the error boundary (but not anything above it).
expect(container.innerHTML).toBe('<p>A</p><h1>Fixed!</h1><p>B</p>');
expect(container.firstChild).toBe(firstP);
expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
// Verify next hot reload doesn't remount anything.
const helloNode = container.firstChild.nextSibling;
patch(() => {
function Hello() {
return <h1>Nice.</h1>;
}$RefreshReg$(Hello, 'Hello');
});expect(container.firstChild.nextSibling).toBe(helloNode);
expect(helloNode.textContent).toBe('Nice.');
}});it('remounts failed error boundaries (getDerivedStateFromError)', () => {if (__DEV__) {render(() => {function Hello() {return <h1>Hi</h1>;}$RefreshReg$(Hello, 'Hello');
class Boundary extends React.Component {
state = {error: null};static getDerivedStateFromError(error) {return {error};}render() {if (this.state.error) {
return <h1>Oops: {this.state.error.message}</h1>;
}return this.props.children;
}}function App() {return (<><p>A</p><Boundary><Hello /></Boundary><p>B</p></>);}return App;});expect(container.innerHTML).toBe('<p>A</p><h1>Hi</h1><p>B</p>');
const firstP = container.firstChild;
const secondP = firstP.nextSibling.nextSibling;
// Perform a hot update that fails.
patch(() => {function Hello() {throw new Error('No');}$RefreshReg$(Hello, 'Hello');
});expect(container.innerHTML).toBe('<p>A</p><h1>Oops: No</h1><p>B</p>');
expect(container.firstChild).toBe(firstP);
expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
// Perform a hot update that fixes the error.
patch(() => {function Hello() {return <h1>Fixed!</h1>;}$RefreshReg$(Hello, 'Hello');
});// This should remount the error boundary (but not anything above it).
expect(container.innerHTML).toBe('<p>A</p><h1>Fixed!</h1><p>B</p>');
expect(container.firstChild).toBe(firstP);
expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
// Verify next hot reload doesn't remount anything.
const helloNode = container.firstChild.nextSibling;
patch(() => {function Hello() {return <h1>Nice.</h1>;
}$RefreshReg$(Hello, 'Hello');
});expect(container.firstChild.nextSibling).toBe(helloNode);
expect(helloNode.textContent).toBe('Nice.');
}});it('remounts error boundaries that failed asynchronously after hot update', () => {if (__DEV__) {render(() => {function Hello() {const [x] = React.useState('');
React.useEffect(() => {}, []);
x.slice(); // Doesn't throw initially.
return <h1>Hi</h1>;}$RefreshReg$(Hello, 'Hello');
class Boundary extends React.Component {
state = {error: null};static getDerivedStateFromError(error) {return {error};}render() {if (this.state.error) {
return <h1>Oops: {this.state.error.message}</h1>;
}return this.props.children;
}}function App() {return (<><p>A</p><Boundary><Hello /></Boundary><p>B</p></>);}return App;});expect(container.innerHTML).toBe('<p>A</p><h1>Hi</h1><p>B</p>');
const firstP = container.firstChild;
const secondP = firstP.nextSibling.nextSibling;
// Perform a hot update that fails.
act(() => {patch(() => {function Hello() {const [x, setX] = React.useState('');
React.useEffect(() => {
setTimeout(() => {setX(42); // This will crash next render.
}, 1);}, []);
x.slice();
return <h1>Hi</h1>;}$RefreshReg$(Hello, 'Hello');
});});expect(container.innerHTML).toBe('<p>A</p><h1>Hi</h1><p>B</p>');
// Run timeout inside effect:act(() => {jest.runAllTimers();
});expect(container.innerHTML).toBe(
'<p>A</p><h1>Oops: x.slice is not a function</h1><p>B</p>',
);expect(container.firstChild).toBe(firstP);
expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
// Perform a hot update that fixes the error.
act(() => {patch(() => {function Hello() {const [x] = React.useState('');
React.useEffect(() => {}, []); // Removes the bad effect code.
x.slice(); // Doesn't throw initially.
return <h1>Fixed!</h1>;}$RefreshReg$(Hello, 'Hello');
});});// This should remount the error boundary (but not anything above it).
expect(container.innerHTML).toBe('<p>A</p><h1>Fixed!</h1><p>B</p>');
expect(container.firstChild).toBe(firstP);
expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
// Verify next hot reload doesn't remount anything.
const helloNode = container.firstChild.nextSibling;
act(() => {patch(() => {function Hello() {const [x] = React.useState('');
React.useEffect(() => {}, []);
x.slice();
return <h1>Nice.</h1>;
}$RefreshReg$(Hello, 'Hello');
});});expect(container.firstChild.nextSibling).toBe(helloNode);
expect(helloNode.textContent).toBe('Nice.');
}});it('remounts a failed root on mount', () => {if (__DEV__) {expect(() => {render(() => {function Hello() {throw new Error('No');}$RefreshReg$(Hello, 'Hello');
return Hello;});}).toThrow('No');
expect(container.innerHTML).toBe('');
// A bad retryexpect(() => {patch(() => {function Hello() {throw new Error('Not yet');}$RefreshReg$(Hello, 'Hello');
});}).toThrow('Not yet');
expect(container.innerHTML).toBe('');
// Perform a hot update that fixes the error.
patch(() => {function Hello() {return <h1>Fixed!</h1>;}$RefreshReg$(Hello, 'Hello');
});// This should mount the root.
expect(container.innerHTML).toBe('<h1>Fixed!</h1>');
// Ensure we can keep failing and recovering later.
expect(() => {patch(() => {function Hello() {throw new Error('No 2');}$RefreshReg$(Hello, 'Hello');
});}).toThrow('No 2');
expect(container.innerHTML).toBe('');
expect(() => {patch(() => {function Hello() {throw new Error('Not yet 2');}$RefreshReg$(Hello, 'Hello');
});}).toThrow('Not yet 2');
expect(container.innerHTML).toBe('');
patch(() => {function Hello() {return <h1>Fixed 2!</h1>;}$RefreshReg$(Hello, 'Hello');
});expect(container.innerHTML).toBe('<h1>Fixed 2!</h1>');
// Updates after intentional unmount are ignored.
ReactDOM.unmountComponentAtNode(container);
patch(() => {function Hello() {throw new Error('Ignored');}$RefreshReg$(Hello, 'Hello');
});expect(container.innerHTML).toBe('');
patch(() => {function Hello() {return <h1>Ignored</h1>;}$RefreshReg$(Hello, 'Hello');
});expect(container.innerHTML).toBe('');
}});it('does not retry an intentionally unmounted failed root', () => {if (__DEV__) {expect(() => {render(() => {function Hello() {throw new Error('No');}$RefreshReg$(Hello, 'Hello');
return Hello;});}).toThrow('No');
expect(container.innerHTML).toBe('');
// Intentional unmount.
ReactDOM.unmountComponentAtNode(container);
// Perform a hot update that fixes the error.
patch(() => {function Hello() {return <h1>Fixed!</h1>;}$RefreshReg$(Hello, 'Hello');
});// This should stay unmounted.
expect(container.innerHTML).toBe('');
}});it('remounts a failed root on update', () => {if (__DEV__) {render(() => {function Hello() {return <h1>Hi</h1>;}$RefreshReg$(Hello, 'Hello');
return Hello;});expect(container.innerHTML).toBe('<h1>Hi</h1>');
// Perform a hot update that fails.
// This removes the root.
expect(() => {patch(() => {function Hello() {throw new Error('No');}$RefreshReg$(Hello, 'Hello');
});}).toThrow('No');
expect(container.innerHTML).toBe('');
// A bad retryexpect(() => {patch(() => {function Hello() {throw new Error('Not yet');}$RefreshReg$(Hello, 'Hello');
});}).toThrow('Not yet');
expect(container.innerHTML).toBe('');
// Perform a hot update that fixes the error.
patch(() => {function Hello() {return <h1>Fixed!</h1>;}$RefreshReg$(Hello, 'Hello');
});// This should remount the root.
expect(container.innerHTML).toBe('<h1>Fixed!</h1>');
// Verify next hot reload doesn't remount anything.
const helloNode = container.firstChild;
patch(() => {function Hello() {return <h1>Nice.</h1>;
}$RefreshReg$(Hello, 'Hello');
});expect(container.firstChild).toBe(helloNode);
expect(helloNode.textContent).toBe('Nice.');
// Break again.
expect(() => {patch(() => {function Hello() {throw new Error('Oops');}$RefreshReg$(Hello, 'Hello');
});}).toThrow('Oops');
expect(container.innerHTML).toBe('');
// Perform a hot update that fixes the error.
patch(() => {function Hello() {return <h1>At last.</h1>;
}$RefreshReg$(Hello, 'Hello');
});// This should remount the root.
expect(container.innerHTML).toBe('<h1>At last.</h1>');
// Check we don't attempt to reverse an intentional unmount.
ReactDOM.unmountComponentAtNode(container);
expect(container.innerHTML).toBe('');
patch(() => {function Hello() {return <h1>Never mind me!</h1>;}$RefreshReg$(Hello, 'Hello');
});expect(container.innerHTML).toBe('');
// Mount a new container.
render(() => {function Hello() {return <h1>Hi</h1>;}$RefreshReg$(Hello, 'Hello');
return Hello;});expect(container.innerHTML).toBe('<h1>Hi</h1>');
// Break again.
expect(() => {patch(() => {function Hello() {throw new Error('Oops');}$RefreshReg$(Hello, 'Hello');
});}).toThrow('Oops');
expect(container.innerHTML).toBe('');
// Check we don't attempt to reverse an intentional unmount, even after an error.
ReactDOM.unmountComponentAtNode(container);
expect(container.innerHTML).toBe('');
patch(() => {function Hello() {return <h1>Never mind me!</h1>;}$RefreshReg$(Hello, 'Hello');
});expect(container.innerHTML).toBe('');
}});it('regression test: does not get into an infinite loop', () => {if (__DEV__) {const containerA = document.createElement('div');
const containerB = document.createElement('div');
// Initially, nothing interesting.
const RootAV1 = () => {return 'A1';};$RefreshReg$(RootAV1, 'RootA');
const RootBV1 = () => {return 'B1';};$RefreshReg$(RootBV1, 'RootB');
act(() => {ReactDOM.render(<RootAV1 />, containerA);
ReactDOM.render(<RootBV1 />, containerB);
});expect(containerA.innerHTML).toBe('A1');
expect(containerB.innerHTML).toBe('B1');
// Then make the first root fail.
const RootAV2 = () => {throw new Error('A2!');};$RefreshReg$(RootAV2, 'RootA');
expect(() => ReactFreshRuntime.performReactRefresh()).toThrow('A2!');
expect(containerA.innerHTML).toBe('');
expect(containerB.innerHTML).toBe('B1');
// Then patch the first root, but make it fail in the commit phase.
// This used to trigger an infinite loop due to a list of failed roots// being mutated while it was being iterated on.
const RootAV3 = () => {React.useLayoutEffect(() => {
throw new Error('A3!');}, []);
return 'A3';};$RefreshReg$(RootAV3, 'RootA');
expect(() => ReactFreshRuntime.performReactRefresh()).toThrow('A3!');
expect(containerA.innerHTML).toBe('');
expect(containerB.innerHTML).toBe('B1');
const RootAV4 = () => {return 'A4';};$RefreshReg$(RootAV4, 'RootA');
ReactFreshRuntime.performReactRefresh();
expect(containerA.innerHTML).toBe('A4');
expect(containerB.innerHTML).toBe('B1');
}});it('remounts classes on every edit', () => {if (__DEV__) {const HelloV1 = render(() => {class Hello extends React.Component {
state = {count: 0};handleClick = () => {this.setState(prev => ({
count: prev.count + 1,
}));};render() {return (<p style={{color: 'blue'}} onClick={this.handleClick}>
{this.state.count}
</p>);}}// For classes, we wouldn't do this call via Babel plugin.
// Instead, we'd do it at module boundaries.
// Normally classes would get a different type and remount anyway,// but at module boundaries we may want to prevent propagation.
// However we still want to force a remount and use latest version.
$RefreshReg$(Hello, 'Hello');
return Hello;});// Bump the state before patching.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('1');
// Perform a hot update.
const HelloV2 = patch(() => {class Hello extends React.Component {
state = {count: 0};handleClick = () => {this.setState(prev => ({
count: prev.count + 1,
}));};render() {return (<p style={{color: 'red'}} onClick={this.handleClick}>
{this.state.count}
</p>);}}$RefreshReg$(Hello, 'Hello');
return Hello;});// It should have remounted the class.
expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('0');
expect(newEl.style.color).toBe('red');
act(() => {newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(newEl.textContent).toBe('1');
// Now top-level renders of both types resolve to latest.
render(() => HelloV1);render(() => HelloV2);expect(container.firstChild).toBe(newEl);
expect(newEl.style.color).toBe('red');
expect(newEl.textContent).toBe('1');
const HelloV3 = patch(() => {class Hello extends React.Component {
state = {count: 0};handleClick = () => {this.setState(prev => ({
count: prev.count + 1,
}));};render() {return (<p style={{color: 'orange'}} onClick={this.handleClick}>
{this.state.count}
</p>);}}$RefreshReg$(Hello, 'Hello');
return Hello;});// It should have remounted the class again.
expect(container.firstChild).not.toBe(el);
const finalEl = container.firstChild;
expect(finalEl.textContent).toBe('0');
expect(finalEl.style.color).toBe('orange');
act(() => {finalEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(finalEl.textContent).toBe('1');
render(() => HelloV3);render(() => HelloV2);render(() => HelloV1);expect(container.firstChild).toBe(finalEl);
expect(finalEl.style.color).toBe('orange');
expect(finalEl.textContent).toBe('1');
}});it('updates refs when remounting', () => {if (__DEV__) {const testRef = React.createRef();
render(() => {class Hello extends React.Component {
getColor() {return 'green';}render() {return <p />;}}$RefreshReg$(Hello, 'Hello');
return Hello;},{ref: testRef},);expect(testRef.current.getColor()).toBe('green');
patch(() => {class Hello extends React.Component {
getColor() {return 'orange';}render() {return <p />;}}$RefreshReg$(Hello, 'Hello');
});expect(testRef.current.getColor()).toBe('orange');
patch(() => {const Hello = React.forwardRef((props, ref) => {
React.useImperativeHandle(ref, () => ({
getColor() {return 'pink';},}));return <p />;});$RefreshReg$(Hello, 'Hello');
});expect(testRef.current.getColor()).toBe('pink');
patch(() => {const Hello = React.forwardRef((props, ref) => {
React.useImperativeHandle(ref, () => ({
getColor() {return 'yellow';},}));return <p />;});$RefreshReg$(Hello, 'Hello');
});expect(testRef.current.getColor()).toBe('yellow');
patch(() => {const Hello = React.forwardRef((props, ref) => {
React.useImperativeHandle(ref, () => ({
getColor() {return 'yellow';},}));return <p />;});$RefreshReg$(Hello, 'Hello');
});expect(testRef.current.getColor()).toBe('yellow');
}});it('remounts on conversion from class to function and back', () => {if (__DEV__) {const HelloV1 = render(() => {function Hello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}</p>);}$RefreshReg$(Hello, 'Hello');
return Hello;});// Bump the state before patching.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('1');
// Perform a hot update that turns it into a class.
const HelloV2 = patch(() => {class Hello extends React.Component {
state = {count: 0};handleClick = () => {this.setState(prev => ({
count: prev.count + 1,
}));};render() {return (<p style={{color: 'red'}} onClick={this.handleClick}>
{this.state.count}
</p>);}}$RefreshReg$(Hello, 'Hello');
return Hello;});// It should have remounted.
expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('0');
expect(newEl.style.color).toBe('red');
act(() => {newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(newEl.textContent).toBe('1');
// Now top-level renders of both types resolve to latest.
render(() => HelloV1);render(() => HelloV2);expect(container.firstChild).toBe(newEl);
expect(newEl.style.color).toBe('red');
expect(newEl.textContent).toBe('1');
// Now convert it back to a function.
const HelloV3 = patch(() => {function Hello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{val}</p>);}$RefreshReg$(Hello, 'Hello');
return Hello;});// It should have remounted again.
expect(container.firstChild).not.toBe(el);
const finalEl = container.firstChild;
expect(finalEl.textContent).toBe('0');
expect(finalEl.style.color).toBe('orange');
act(() => {finalEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(finalEl.textContent).toBe('1');
render(() => HelloV3);render(() => HelloV2);render(() => HelloV1);expect(container.firstChild).toBe(finalEl);
expect(finalEl.style.color).toBe('orange');
expect(finalEl.textContent).toBe('1');
// Now that it's a function, verify edits keep state.
patch(() => {function Hello() {const [val, setVal] = React.useState(0);
return (<p style={{color: 'purple'}} onClick={() => setVal(val + 1)}>
{val}</p>);}$RefreshReg$(Hello, 'Hello');
return Hello;});expect(container.firstChild).toBe(finalEl);
expect(finalEl.style.color).toBe('purple');
expect(finalEl.textContent).toBe('1');
}});it('can find host instances for a family', () => {if (__DEV__) {render(() => {function Child({children}) {return <div className="Child">{children}</div>;}$RefreshReg$(Child, 'Child');
function Parent({children}) {return (<div className="Parent"><div><Child /></div><div><Child /></div></div>);}$RefreshReg$(Parent, 'Parent');
function App() {return (<div className="App"><Parent /><Cls><Parent /></Cls><Indirection><Empty /></Indirection></div>);}$RefreshReg$(App, 'App');
class Cls extends React.Component {
render() {return this.props.children;
}}function Indirection({children}) {return children;}function Empty() {return null;}$RefreshReg$(Empty, 'Empty');
function Frag() {return (<><div className="Frag"><div /></div><div className="Frag"><div /></div></>);}$RefreshReg$(Frag, 'Frag');
return App;});const parentFamily = ReactFreshRuntime.getFamilyByID('Parent');
const childFamily = ReactFreshRuntime.getFamilyByID('Child');
const emptyFamily = ReactFreshRuntime.getFamilyByID('Empty');
testFindHostInstancesForFamilies([parentFamily],
container.querySelectorAll('.Parent'),
);testFindHostInstancesForFamilies([childFamily],
container.querySelectorAll('.Child'),
);// When searching for both Parent and Child,// we'll stop visual highlighting at the Parent.
testFindHostInstancesForFamilies([parentFamily, childFamily],
container.querySelectorAll('.Parent'),
);// When we can't find host nodes, use the closest parent.
testFindHostInstancesForFamilies([emptyFamily],
container.querySelectorAll('.App'),
);}});function testFindHostInstancesForFamilies(families, expectedNodes) {const foundInstances = Array.from(
ReactFreshRuntime.findAffectedHostInstances(families),
);expect(foundInstances.length).toEqual(expectedNodes.length);
foundInstances.forEach((node, i) => {
expect(node).toBe(expectedNodes[i]);
});}it('can update multiple roots independently', () => {if (__DEV__) {// Declare the first version.
const HelloV1 = () => {const [val, setVal] = React.useState(0);
return (<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}</p>);};$RefreshReg$(HelloV1, 'Hello');
// Perform a hot update before any roots exist.
const HelloV2 = () => {const [val, setVal] = React.useState(0);
return (<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}</p>);};$RefreshReg$(HelloV2, 'Hello');
ReactFreshRuntime.performReactRefresh();
// Mount three roots.
const cont1 = document.createElement('div');
const cont2 = document.createElement('div');
const cont3 = document.createElement('div');
document.body.appendChild(cont1);
document.body.appendChild(cont2);
document.body.appendChild(cont3);
try {ReactDOM.render(<HelloV1 id={1} />, cont1);
ReactDOM.render(<HelloV2 id={2} />, cont2);
ReactDOM.render(<HelloV1 id={3} />, cont3);
// Expect we see the V2 color.
expect(cont1.firstChild.style.color).toBe('red');
expect(cont2.firstChild.style.color).toBe('red');
expect(cont3.firstChild.style.color).toBe('red');
expect(cont1.firstChild.textContent).toBe('0');
expect(cont2.firstChild.textContent).toBe('0');
expect(cont3.firstChild.textContent).toBe('0');
// Bump the state for each of them.
act(() => {cont1.firstChild.dispatchEvent(
new MouseEvent('click', {bubbles: true}),);cont2.firstChild.dispatchEvent(
new MouseEvent('click', {bubbles: true}),);cont3.firstChild.dispatchEvent(
new MouseEvent('click', {bubbles: true}),);});expect(cont1.firstChild.style.color).toBe('red');
expect(cont2.firstChild.style.color).toBe('red');
expect(cont3.firstChild.style.color).toBe('red');
expect(cont1.firstChild.textContent).toBe('1');
expect(cont2.firstChild.textContent).toBe('1');
expect(cont3.firstChild.textContent).toBe('1');
// Perform another hot update.
const HelloV3 = () => {const [val, setVal] = React.useState(0);
return (<p style={{color: 'green'}} onClick={() => setVal(val + 1)}>
{val}</p>);};$RefreshReg$(HelloV3, 'Hello');
ReactFreshRuntime.performReactRefresh();
// It should affect all roots.
expect(cont1.firstChild.style.color).toBe('green');
expect(cont2.firstChild.style.color).toBe('green');
expect(cont3.firstChild.style.color).toBe('green');
expect(cont1.firstChild.textContent).toBe('1');
expect(cont2.firstChild.textContent).toBe('1');
expect(cont3.firstChild.textContent).toBe('1');
// Unmount the second root.
ReactDOM.unmountComponentAtNode(cont2);
// Make the first root throw and unmount on hot update.
const HelloV4 = ({id}) => {if (id === 1) {throw new Error('Oops.');
}const [val, setVal] = React.useState(0);
return (<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{val}</p>);};$RefreshReg$(HelloV4, 'Hello');
expect(() => {ReactFreshRuntime.performReactRefresh();
}).toThrow('Oops.');
// Still, we expect the last root to be updated.
expect(cont1.innerHTML).toBe('');
expect(cont2.innerHTML).toBe('');
expect(cont3.firstChild.style.color).toBe('orange');
expect(cont3.firstChild.textContent).toBe('1');
} finally {document.body.removeChild(cont1);
document.body.removeChild(cont2);
document.body.removeChild(cont3);
}}});// Module runtimes can use this to decide whether
// to propagate an update up to the modules that imported it,
// or to stop at the current module because it's a component.
// This can't and doesn't need to be 100% precise.
it('can detect likely component types', () => {
function useTheme() {}
function Widget() {}
if (__DEV__) {
expect(ReactFreshRuntime.isLikelyComponentType(false)).toBe(false);
expect(ReactFreshRuntime.isLikelyComponentType(null)).toBe(false);
expect(ReactFreshRuntime.isLikelyComponentType('foo')).toBe(false);
// We need to hit a balance here.
// If we lean towards assuming everything is a component,
// editing modules that export plain functions won't trigger
// a proper reload because we will bottle up the update.
// So we're being somewhat conservative.
expect(ReactFreshRuntime.isLikelyComponentType(() => {})).toBe(false);
expect(ReactFreshRuntime.isLikelyComponentType(function () {})).toBe(
false,
);expect(
ReactFreshRuntime.isLikelyComponentType(function lightenColor() {}),
).toBe(false);
const loadUser = () => {};
expect(ReactFreshRuntime.isLikelyComponentType(loadUser)).toBe(false);
const useStore = () => {};
expect(ReactFreshRuntime.isLikelyComponentType(useStore)).toBe(false);
expect(ReactFreshRuntime.isLikelyComponentType(useTheme)).toBe(false);
const rogueProxy = new Proxy(
{},{get(target, property) {
throw new Error();
},},);expect(ReactFreshRuntime.isLikelyComponentType(rogueProxy)).toBe(false);
// These seem like function components.
const Button = () => {};
expect(ReactFreshRuntime.isLikelyComponentType(Button)).toBe(true);
expect(ReactFreshRuntime.isLikelyComponentType(Widget)).toBe(true);
const ProxyButton = new Proxy(Button, {
get(target, property) {
return target[property];
},});expect(ReactFreshRuntime.isLikelyComponentType(ProxyButton)).toBe(true);
const anon = (() => () => {})();
anon.displayName = 'Foo';
expect(ReactFreshRuntime.isLikelyComponentType(anon)).toBe(true);
// These seem like class components.
class Btn extends React.Component {}
class PureBtn extends React.PureComponent {}
const ProxyBtn = new Proxy(Btn, {
get(target, property) {
return target[property];
},});expect(ReactFreshRuntime.isLikelyComponentType(Btn)).toBe(true);
expect(ReactFreshRuntime.isLikelyComponentType(PureBtn)).toBe(true);
expect(ReactFreshRuntime.isLikelyComponentType(ProxyBtn)).toBe(true);
expect(
ReactFreshRuntime.isLikelyComponentType(
createReactClass({render() {}}),
),).toBe(true);
// These don't.
class Figure {
move() {}
}expect(ReactFreshRuntime.isLikelyComponentType(Figure)).toBe(false);
class Point extends Figure {}
expect(ReactFreshRuntime.isLikelyComponentType(Point)).toBe(false);
// Run the same tests without Babel.
// This tests real arrow functions and classes, as implemented in Node.
// eslint-disable-next-line no-new-func
new Function(
'global',
'React',
'ReactFreshRuntime',
'expect',
'createReactClass',
`expect(ReactFreshRuntime.isLikelyComponentType(() => {})).toBe(false);expect(ReactFreshRuntime.isLikelyComponentType(function() {})).toBe(false);expect(ReactFreshRuntime.isLikelyComponentType(function lightenColor() {}),).toBe(false);const loadUser = () => {};expect(ReactFreshRuntime.isLikelyComponentType(loadUser)).toBe(false);const useStore = () => {};expect(ReactFreshRuntime.isLikelyComponentType(useStore)).toBe(false);function useTheme() {}expect(ReactFreshRuntime.isLikelyComponentType(useTheme)).toBe(false);// These seem like function components.let Button = () => {};expect(ReactFreshRuntime.isLikelyComponentType(Button)).toBe(true);function Widget() {}expect(ReactFreshRuntime.isLikelyComponentType(Widget)).toBe(true);let anon = (() => () => {})();anon.displayName = 'Foo';expect(ReactFreshRuntime.isLikelyComponentType(anon)).toBe(true);// These seem like class components.class Btn extends React.Component {}class PureBtn extends React.PureComponent {}expect(ReactFreshRuntime.isLikelyComponentType(Btn)).toBe(true);expect(ReactFreshRuntime.isLikelyComponentType(PureBtn)).toBe(true);expect(ReactFreshRuntime.isLikelyComponentType(createReactClass({render() {}})),).toBe(true);// These don't.class Figure {move() {}}expect(ReactFreshRuntime.isLikelyComponentType(Figure)).toBe(false);class Point extends Figure {}expect(ReactFreshRuntime.isLikelyComponentType(Point)).toBe(false);`,
)(global, React, ReactFreshRuntime, expect, createReactClass);
}});it('reports updated and remounted families to the caller', () => {
if (__DEV__) {
const HelloV1 = () => {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);};$RefreshReg$(HelloV1, 'Hello');
const HelloV2 = () => {const [val, setVal] = React.useState(0);
return (<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}</p>);};$RefreshReg$(HelloV2, 'Hello');
const update = ReactFreshRuntime.performReactRefresh();
expect(update.updatedFamilies.size).toBe(1);
expect(update.staleFamilies.size).toBe(0);
const family = update.updatedFamilies.values().next().value;
expect(family.current.name).toBe('HelloV2');
// For example, we can use this to print a log of what was updated.
}});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', async () => {
if (__DEV__) {
initFauxDevToolsHook();
// Load these first, as if they're coming from a CDN.
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
act = require('react-dom/test-utils').act;
internalAct = require('internal-test-utils').act;
// Important! Inject into the global hook *after* ReactDOM runs:
ReactFreshRuntime = require('react-refresh/runtime');
ReactFreshRuntime.injectIntoGlobalHook(global);
// We're verifying that we're able to track roots mounted after this point.
// The rest of this test is taken from the simplest first test case.
render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
return Hello;});// Bump the state before patching.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});expect(el.textContent).toBe('1');
// Perform a hot update.
patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);}$RefreshReg$(Hello, 'Hello');
return Hello;});// Assert the state was preserved but color changed.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
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('scheduler', () => jest.requireActual('scheduler-0-13'));
jest.mock('scheduler/tracing', () =>
jest.requireActual('scheduler-0-13/tracing'),
);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 <div>Hi!</div>;
}$RefreshReg$(Hello, 'Hello');
return Hello;});expect(onCommitFiberRoot).toHaveBeenCalled();
}});});