'use strict';
let React;
let ReactDOM;
let ReactDOMClient;
let ReactFreshRuntime;
let Scheduler;
let act;
let createReactClass;
let waitFor;
let assertLog;
describe('ReactFresh', () => {
let container;
let root;
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('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');
root = ReactDOMClient.createRoot(container);
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;
}
async function render(version, props) {
const Component = version();
await act(() => {
root.render(<Component {...props} />);
});
return Component;
}
async function patch(version) {
const Component = version();
await act(() => {
ReactFreshRuntime.performReactRefresh();
});
return Component;
}
function patchSync(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;
}
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', async () => {
if (__DEV__) {
const HelloV1 = await 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;
});
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
const HelloV2 = await 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;
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
await render(() => HelloV1);
await render(() => HelloV2);
await render(() => HelloV1);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('3');
expect(el.style.color).toBe('red');
await render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
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', async () => {
if (__DEV__) {
const OuterV1 = await 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;
});
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
const OuterV2 = await 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;
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
await render(() => OuterV1);
await render(() => OuterV2);
await render(() => OuterV1);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
await 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;
});
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', async () => {
if (__DEV__) {
const ParentV1 = await 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 />;
}
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},
);
let el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
await render(() => ParentV1, {cond: false});
expect(el).not.toBe(container.firstChild);
el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
await render(() => ParentV1, {cond: true});
expect(el).not.toBe(container.firstChild);
el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
const ParentV2 = await 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 />;
}
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;
});
expect(el).toBe(container.firstChild);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
await render(() => ParentV2, {cond: false});
expect(el).not.toBe(container.firstChild);
el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('red');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el).toBe(container.firstChild);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
await render(() => ParentV1);
await render(() => ParentV2);
await 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', async () => {
if (__DEV__) {
await 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;
});
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
await 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;
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
}
});
it('can update forwardRef render function in isolation', async () => {
if (__DEV__) {
await 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);
});
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
await 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');
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
}
});
it('can preserve state for simple memo', async () => {
if (__DEV__) {
const OuterV1 = await 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;
});
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
const OuterV2 = await 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;
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
await render(() => OuterV1);
await render(() => OuterV2);
await render(() => OuterV1);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
await 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;
});
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', async () => {
if (__DEV__) {
const OuterV1 = await 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;
});
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
const OuterV2 = await 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;
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
await render(() => OuterV1);
await render(() => OuterV2);
await render(() => OuterV1);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
await 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;
});
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', async () => {
if (__DEV__) {
await 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);
});
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
await patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
}
});
it('can preserve state for memo(forwardRef)', async () => {
if (__DEV__) {
const OuterV1 = await 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;
});
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
const OuterV2 = await 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;
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
await render(() => OuterV1);
await render(() => OuterV2);
await render(() => OuterV1);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
await 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;
});
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__) {
let resolve;
const AppV1 = await 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 => {
resolve = () => _resolve({default: Hello});
}),
);
$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(() => {
resolve();
});
expect(container.textContent).toBe('0');
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
const AppV2 = await 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 => {
resolve = () => _resolve({default: Hello});
}),
);
$RefreshReg$(Outer, 'Outer');
function App() {
return (
<React.Suspense fallback={<p>Loading</p>}>
<Outer />
</React.Suspense>
);
}
$RefreshReg$(App, 'App');
return App;
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
await render(() => AppV1);
await render(() => AppV2);
await render(() => AppV1);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('2');
expect(el.style.color).toBe('red');
await 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() {
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__) {
let resolve;
await 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 => {
resolve = () => _resolve({default: Hello});
}),
);
$RefreshReg$(Outer, 'Outer');
function App() {
return (
<React.Suspense fallback={<p>Loading</p>}>
<Outer />
</React.Suspense>
);
}
return App;
});
expect(container.textContent).toBe('Loading');
await 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(() => {
resolve();
});
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('red');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
await 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__) {
let resolve;
await 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 => {
resolve = () => _resolve({default: Hello});
}),
);
$RefreshReg$(Outer, 'Outer');
function App() {
return (
<React.Suspense fallback={<p>Loading</p>}>
<Outer />
</React.Suspense>
);
}
return App;
});
expect(container.textContent).toBe('Loading');
await 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(() => {
resolve();
});
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('red');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
await 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__) {
let resolve;
await 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 => {
resolve = () => _resolve({default: Hello});
}),
);
$RefreshReg$(Outer, 'Outer');
function App() {
return (
<React.Suspense fallback={<p>Loading</p>}>
<Outer />
</React.Suspense>
);
}
return App;
});
expect(container.textContent).toBe('Loading');
await 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(() => {
resolve();
});
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('red');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
await 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__) {
let resolve;
await 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 => {
resolve = () => _resolve({default: Hello});
}),
);
$RefreshReg$(Outer, 'Outer');
function App() {
return (
<React.Suspense fallback={<p>Loading</p>}>
<Outer />
</React.Suspense>
);
}
return App;
});
expect(container.textContent).toBe('Loading');
await 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(() => {
resolve();
});
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('red');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
await 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('only patches the fallback tree while suspended', async () => {
if (__DEV__) {
const AppV1 = await 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},
);
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('');
await 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('');
await 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('');
await render(() => AppV1, {shouldSuspend: true});
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('');
await 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('');
await 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');
});
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('red');
expect(fallbackChild.style.display).toBe('');
await 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('');
await 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', async () => {
if (__DEV__) {
let appRenders = 0;
await 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);
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
expect(appRenders).toBe(1);
await patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
expect(appRenders).toBe(1);
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('2');
expect(appRenders).toBe(1);
}
});
it('batches re-renders during a hot update', async () => {
if (__DEV__) {
let helloRenders = 0;
await 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;
await 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', async () => {
if (__DEV__) {
const AppV1 = await 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},
);
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
await render(() => AppV1, {cond: true});
const el2 = container.firstChild;
expect(el2).not.toBe(el);
expect(el2.textContent).toBe('0');
expect(el2.style.color).toBe('blue');
await act(() => {
el2.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el2.textContent).toBe('1');
await 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');
});
expect(container.firstChild).toBe(el2);
expect(el2.textContent).toBe('1');
expect(el2.style.color).toBe('red');
await 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', async () => {
if (__DEV__) {
const HelloV1 = await render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '1');
return Hello;
});
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
const HelloV2 = await patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '1');
return Hello;
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
const HelloV3 = await patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'yellow'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '2');
return Hello;
});
expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('0');
expect(newEl.style.color).toBe('yellow');
await act(() => {
newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(newEl.textContent).toBe('1');
expect(newEl.style.color).toBe('yellow');
await render(() => HelloV1);
await render(() => HelloV2);
await render(() => HelloV3);
await render(() => HelloV2);
await render(() => HelloV1);
expect(container.firstChild).toBe(newEl);
expect(newEl.textContent).toBe('1');
expect(newEl.style.color).toBe('yellow');
await patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'purple'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '2');
return Hello;
});
expect(container.firstChild).toBe(newEl);
expect(newEl.textContent).toBe('1');
expect(newEl.style.color).toBe('purple');
await 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;
});
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', async () => {
if (__DEV__) {
const HelloV1 = prepare(() => {
function Hello() {
return null;
}
$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '1');
return Hello;
});
const Bailout = React.memo(({children}) => {
return children;
});
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>,
];
await act(() => {
root.render(null);
});
for (let i = 0; i < trees.length; i++) {
await runRemountingStressTest(trees[i]);
}
for (let i = 0; i < trees.length; i++) {
for (let j = 0; j < trees.length; j++) {
await act(() => {
root.render(null);
});
await runRemountingStressTest(trees[i]);
await runRemountingStressTest(trees[j]);
await runRemountingStressTest(trees[i]);
}
}
}
}, 10000);
async function runRemountingStressTest(tree) {
await patch(() => {
function Hello({children}) {
return <section data-color="blue">{children}</section>;
}
$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '1');
return Hello;
});
await act(() => {
root.render(tree);
});
const elements = container.querySelectorAll('section');
expect(elements.length).toBe(3);
elements.forEach(el => {
expect(el.dataset.color).toBe('blue');
});
await 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) => {
expect(el).toBe(elements[index]);
expect(el.dataset.color).toBe('red');
});
await patch(() => {
function Hello({children}) {
return <section data-color="orange">{children}</section>;
}
$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '2');
return Hello;
});
const elementsAfterRemount = container.querySelectorAll('section');
expect(elementsAfterRemount.length).toBe(3);
elementsAfterRemount.forEach((el, index) => {
expect(el).not.toBe(elements[index]);
expect(el.dataset.color).toBe('orange');
});
await patch(() => {
function Hello({children}) {
return <section data-color="black">{children}</section>;
}
$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '2');
return Hello;
});
expect(container.querySelectorAll('section').length).toBe(3);
container.querySelectorAll('section').forEach((el, index) => {
expect(el).toBe(elementsAfterRemount[index]);
expect(el.dataset.color).toBe('black');
});
await act(() => {
root.render(tree);
});
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', async () => {
if (__DEV__) {
await testRemountingWithWrapper(Hello => Hello);
}
});
it('can remount on signature change within a simple memo wrapper', async () => {
if (__DEV__) {
await testRemountingWithWrapper(Hello => React.memo(Hello));
}
});
it('can remount on signature change within a lazy simple memo wrapper', async () => {
if (__DEV__) {
await testRemountingWithWrapper(Hello =>
React.lazy(() => ({
then(cb) {
cb({default: React.memo(Hello)});
},
})),
);
}
});
it('can remount on signature change within forwardRef', async () => {
if (__DEV__) {
await testRemountingWithWrapper(Hello => React.forwardRef(Hello));
}
});
it('can remount on signature change within forwardRef render function', async () => {
if (__DEV__) {
await testRemountingWithWrapper(Hello =>
React.forwardRef(() => <Hello />),
);
}
});
it('can remount on signature change within nested memo', async () => {
if (__DEV__) {
await testRemountingWithWrapper(Hello =>
React.memo(React.memo(React.memo(Hello))),
);
}
});
it('can remount on signature change within a memo wrapper and custom comparison', async () => {
if (__DEV__) {
await testRemountingWithWrapper(Hello => React.memo(Hello, () => true));
}
});
it('can remount on signature change within a class', async () => {
if (__DEV__) {
await testRemountingWithWrapper(Hello => {
const child = <Hello />;
return class Wrapper extends React.PureComponent {
render() {
return child;
}
};
});
}
});
it('can remount on signature change within a context provider', async () => {
if (__DEV__) {
await 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', async () => {
if (__DEV__) {
await 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', async () => {
if (__DEV__) {
await testRemountingWithWrapper(Hello => {
const child = (
<React.Suspense>
<Hello />
</React.Suspense>
);
return function Wrapper() {
return child;
};
});
}
});
it('can remount on signature change within a mode node', async () => {
if (__DEV__) {
await testRemountingWithWrapper(Hello => {
const child = (
<React.StrictMode>
<Hello />
</React.StrictMode>
);
return function Wrapper() {
return child;
};
});
}
});
it('can remount on signature change within a fragment node', async () => {
if (__DEV__) {
await testRemountingWithWrapper(Hello => {
const child = (
<>
<Hello />
</>
);
return function Wrapper() {
return child;
};
});
}
});
it('can remount on signature change within multiple siblings', async () => {
if (__DEV__) {
await testRemountingWithWrapper(Hello => {
const child = (
<>
<>
<React.Fragment />
</>
<Hello />
<React.Fragment />
</>
);
return function Wrapper() {
return child;
};
});
}
});
it('can remount on signature change within a profiler node', async () => {
if (__DEV__) {
await testRemountingWithWrapper(Hello => {
const child = <Hello />;
return function Wrapper() {
return (
<React.Profiler onRender={() => {}} id="foo">
{child}
</React.Profiler>
);
};
});
}
});
async function testRemountingWithWrapper(wrap) {
await render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '1');
return wrap(Hello);
});
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
await patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '1');
return Hello;
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
await patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'yellow'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '2');
return Hello;
});
expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('0');
expect(newEl.style.color).toBe('yellow');
await act(() => {
newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(newEl.textContent).toBe('1');
expect(newEl.style.color).toBe('yellow');
await patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'purple'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '2');
return Hello;
});
expect(container.firstChild).toBe(newEl);
expect(newEl.textContent).toBe('1');
expect(newEl.style.color).toBe('purple');
await 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;
});
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', async () => {
if (__DEV__) {
let useEffectWithEmptyArrayCalls = 0;
await 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;
});
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
expect(useEffectWithEmptyArrayCalls).toBe(1);
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('2');
expect(useEffectWithEmptyArrayCalls).toBe(1);
await 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;
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('10');
expect(el.style.color).toBe('red');
expect(useEffectWithEmptyArrayCalls).toBe(2);
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('red');
expect(useEffectWithEmptyArrayCalls).toBe(2);
}
});
it('does not get into infinite loops during render phase updates', async () => {
if (__DEV__) {
await 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');
await patch(() => {
function Hello() {
const source = React.useMemo(() => ({value: 20}), []);
const [state, setState] = React.useState({value: null});
if (state !== source) {
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');
}
});
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>
);
};
});
root.render(<AppV1 offscreen={true} />);
await waitFor(['App#layout']);
const el = container.firstChild;
expect(el.hidden).toBe(true);
expect(el.firstChild).toBe(null);
patchSync(() => {
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');
});
expect(container.firstChild).toBe(el);
expect(el.hidden).toBe(true);
expect(el.firstChild).toBe(null);
await waitFor(['Hello#layout']);
expect(container.firstChild).toBe(el);
expect(el.firstChild.textContent).toBe('0');
expect(el.firstChild.style.color).toBe('red');
await act(() => {
el.firstChild.dispatchEvent(
new MouseEvent('click', {
bubbles: true,
}),
);
});
assertLog(['Hello#layout']);
expect(el.firstChild.textContent).toBe('1');
expect(el.firstChild.style.color).toBe('red');
patchSync(() => {
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');
});
expect(container.firstChild).toBe(el);
expect(el.firstChild.textContent).toBe('1');
expect(el.firstChild.style.color).toBe('red');
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)', async () => {
if (__DEV__) {
await 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;
await 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);
await patch(() => {
function Hello() {
return <h1>Fixed!</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
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);
const helloNode = container.firstChild.nextSibling;
await 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)', async () => {
if (__DEV__) {
await 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;
await 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);
await patch(() => {
function Hello() {
return <h1>Fixed!</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
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);
const helloNode = container.firstChild.nextSibling;
await 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', async () => {
if (__DEV__) {
await render(() => {
function Hello() {
const [x] = React.useState('');
React.useEffect(() => {}, []);
x.slice();
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;
let crash;
await patch(() => {
function Hello() {
const [x, setX] = React.useState('');
React.useEffect(() => {
crash = () => {
setX(42);
};
}, []);
x.slice();
return <h1>Hi</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.innerHTML).toBe('<p>A</p><h1>Hi</h1><p>B</p>');
await act(() => {
crash();
});
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);
await patch(() => {
function Hello() {
const [x] = React.useState('');
React.useEffect(() => {}, []);
x.slice();
return <h1>Fixed!</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
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);
const helloNode = container.firstChild.nextSibling;
await 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', async () => {
if (__DEV__) {
await expect(
render(() => {
function Hello() {
throw new Error('No');
}
$RefreshReg$(Hello, 'Hello');
return Hello;
}),
).rejects.toThrow('No');
expect(container.innerHTML).toBe('');
await expect(async () => {
await patch(() => {
function Hello() {
throw new Error('Not yet');
}
$RefreshReg$(Hello, 'Hello');
});
}).rejects.toThrow('Not yet');
expect(container.innerHTML).toBe('');
await patch(() => {
function Hello() {
return <h1>Fixed!</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.innerHTML).toBe('<h1>Fixed!</h1>');
await expect(async () => {
await patch(() => {
function Hello() {
throw new Error('No 2');
}
$RefreshReg$(Hello, 'Hello');
});
}).rejects.toThrow('No 2');
expect(container.innerHTML).toBe('');
await expect(async () => {
await patch(() => {
function Hello() {
throw new Error('Not yet 2');
}
$RefreshReg$(Hello, 'Hello');
});
}).rejects.toThrow('Not yet 2');
expect(container.innerHTML).toBe('');
await patch(() => {
function Hello() {
return <h1>Fixed 2!</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.innerHTML).toBe('<h1>Fixed 2!</h1>');
await act(() => {
root.unmount();
});
await patch(() => {
function Hello() {
throw new Error('Ignored');
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.innerHTML).toBe('');
await patch(() => {
function Hello() {
return <h1>Ignored</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.innerHTML).toBe('');
}
});
it('does not retry an intentionally unmounted failed root', async () => {
if (__DEV__) {
await expect(
render(() => {
function Hello() {
throw new Error('No');
}
$RefreshReg$(Hello, 'Hello');
return Hello;
}),
).rejects.toThrow('No');
expect(container.innerHTML).toBe('');
await act(() => {
root.unmount();
});
await patch(() => {
function Hello() {
return <h1>Fixed!</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.innerHTML).toBe('');
}
});
it('remounts a failed root on update', async () => {
if (__DEV__) {
await render(() => {
function Hello() {
return <h1>Hi</h1>;
}
$RefreshReg$(Hello, 'Hello');
return Hello;
});
expect(container.innerHTML).toBe('<h1>Hi</h1>');
await expect(async () => {
await patch(() => {
function Hello() {
throw new Error('No');
}
$RefreshReg$(Hello, 'Hello');
});
}).rejects.toThrow('No');
expect(container.innerHTML).toBe('');
await expect(async () => {
await patch(() => {
function Hello() {
throw new Error('Not yet');
}
$RefreshReg$(Hello, 'Hello');
});
}).rejects.toThrow('Not yet');
expect(container.innerHTML).toBe('');
await patch(() => {
function Hello() {
return <h1>Fixed!</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.innerHTML).toBe('<h1>Fixed!</h1>');
const helloNode = container.firstChild;
await patch(() => {
function Hello() {
return <h1>Nice.</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.firstChild).toBe(helloNode);
expect(helloNode.textContent).toBe('Nice.');
await expect(async () => {
await patch(() => {
function Hello() {
throw new Error('Oops');
}
$RefreshReg$(Hello, 'Hello');
});
}).rejects.toThrow('Oops');
expect(container.innerHTML).toBe('');
await patch(() => {
function Hello() {
return <h1>At last.</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.innerHTML).toBe('<h1>At last.</h1>');
await act(() => {
root.unmount();
});
expect(container.innerHTML).toBe('');
await patch(() => {
function Hello() {
return <h1>Never mind me!</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.innerHTML).toBe('');
root = ReactDOMClient.createRoot(container);
await render(() => {
function Hello() {
return <h1>Hi</h1>;
}
$RefreshReg$(Hello, 'Hello');
return Hello;
});
expect(container.innerHTML).toBe('<h1>Hi</h1>');
await expect(async () => {
await patch(() => {
function Hello() {
throw new Error('Oops');
}
$RefreshReg$(Hello, 'Hello');
});
}).rejects.toThrow('Oops');
expect(container.innerHTML).toBe('');
await act(() => {
root.unmount();
});
expect(container.innerHTML).toBe('');
await 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', async () => {
if (__DEV__) {
const containerA = document.createElement('div');
const containerB = document.createElement('div');
const rootA = ReactDOMClient.createRoot(containerA);
const rootB = ReactDOMClient.createRoot(containerB);
const RootAV1 = () => {
return 'A1';
};
$RefreshReg$(RootAV1, 'RootA');
const RootBV1 = () => {
return 'B1';
};
$RefreshReg$(RootBV1, 'RootB');
await act(() => {
rootA.render(<RootAV1 />);
rootB.render(<RootBV1 />);
});
expect(containerA.innerHTML).toBe('A1');
expect(containerB.innerHTML).toBe('B1');
const RootAV2 = () => {
throw new Error('A2!');
};
$RefreshReg$(RootAV2, 'RootA');
await expect(
act(() => {
ReactFreshRuntime.performReactRefresh();
}),
).rejects.toThrow('A2!');
expect(containerA.innerHTML).toBe('');
expect(containerB.innerHTML).toBe('B1');
const RootAV3 = () => {
React.useLayoutEffect(() => {
throw new Error('A3!');
}, []);
return 'A3';
};
$RefreshReg$(RootAV3, 'RootA');
await expect(
act(() => {
ReactFreshRuntime.performReactRefresh();
}),
).rejects.toThrow('A3!');
expect(containerA.innerHTML).toBe('');
expect(containerB.innerHTML).toBe('B1');
const RootAV4 = () => {
return 'A4';
};
$RefreshReg$(RootAV4, 'RootA');
await act(() => {
ReactFreshRuntime.performReactRefresh();
});
expect(containerA.innerHTML).toBe('A4');
expect(containerB.innerHTML).toBe('B1');
}
});
it('remounts classes on every edit', async () => {
if (__DEV__) {
const HelloV1 = await 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>
);
}
}
$RefreshReg$(Hello, 'Hello');
return Hello;
});
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
const HelloV2 = await 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;
});
expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('0');
expect(newEl.style.color).toBe('red');
await act(() => {
newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(newEl.textContent).toBe('1');
await render(() => HelloV1);
await render(() => HelloV2);
expect(container.firstChild).toBe(newEl);
expect(newEl.style.color).toBe('red');
expect(newEl.textContent).toBe('1');
const HelloV3 = await 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;
});
expect(container.firstChild).not.toBe(el);
const finalEl = container.firstChild;
expect(finalEl.textContent).toBe('0');
expect(finalEl.style.color).toBe('orange');
await act(() => {
finalEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(finalEl.textContent).toBe('1');
await render(() => HelloV3);
await render(() => HelloV2);
await render(() => HelloV1);
expect(container.firstChild).toBe(finalEl);
expect(finalEl.style.color).toBe('orange');
expect(finalEl.textContent).toBe('1');
}
});
it('updates refs when remounting', async () => {
if (__DEV__) {
const testRef = React.createRef();
await 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');
await patch(() => {
class Hello extends React.Component {
getColor() {
return 'orange';
}
render() {
return <p />;
}
}
$RefreshReg$(Hello, 'Hello');
});
expect(testRef.current.getColor()).toBe('orange');
await patch(() => {
const Hello = React.forwardRef((props, ref) => {
React.useImperativeHandle(ref, () => ({
getColor() {
return 'pink';
},
}));
return <p />;
});
$RefreshReg$(Hello, 'Hello');
});
expect(testRef.current.getColor()).toBe('pink');
await patch(() => {
const Hello = React.forwardRef((props, ref) => {
React.useImperativeHandle(ref, () => ({
getColor() {
return 'yellow';
},
}));
return <p />;
});
$RefreshReg$(Hello, 'Hello');
});
expect(testRef.current.getColor()).toBe('yellow');
await 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', async () => {
if (__DEV__) {
const HelloV1 = await 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;
});
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
const HelloV2 = await 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;
});
expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('0');
expect(newEl.style.color).toBe('red');
await act(() => {
newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(newEl.textContent).toBe('1');
await render(() => HelloV1);
await render(() => HelloV2);
expect(container.firstChild).toBe(newEl);
expect(newEl.style.color).toBe('red');
expect(newEl.textContent).toBe('1');
const HelloV3 = await 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;
});
expect(container.firstChild).not.toBe(el);
const finalEl = container.firstChild;
expect(finalEl.textContent).toBe('0');
expect(finalEl.style.color).toBe('orange');
await act(() => {
finalEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(finalEl.textContent).toBe('1');
await render(() => HelloV3);
await render(() => HelloV2);
await render(() => HelloV1);
expect(container.firstChild).toBe(finalEl);
expect(finalEl.style.color).toBe('orange');
expect(finalEl.textContent).toBe('1');
await 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 update multiple roots independently', async () => {
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');
await act(() => {
ReactFreshRuntime.performReactRefresh();
});
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);
const root1 = ReactDOMClient.createRoot(cont1);
const root2 = ReactDOMClient.createRoot(cont2);
const root3 = ReactDOMClient.createRoot(cont3);
try {
await act(() => {
root1.render(<HelloV1 id={1} />);
});
await act(() => {
root2.render(<HelloV2 id={2} />);
});
await act(() => {
root3.render(<HelloV1 id={3} />);
});
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');
await 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');
const HelloV3 = () => {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'green'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
};
$RefreshReg$(HelloV3, 'Hello');
await act(() => {
ReactFreshRuntime.performReactRefresh();
});
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');
await act(() => {
root2.unmount();
});
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');
await expect(
act(() => {
ReactFreshRuntime.performReactRefresh();
}),
).rejects.toThrow('Oops.');
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);
}
}
});
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);
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);
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);
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);
class Figure {
move() {}
}
expect(ReactFreshRuntime.isLikelyComponentType(Figure)).toBe(false);
class Point extends Figure {}
expect(ReactFreshRuntime.isLikelyComponentType(Point)).toBe(false);
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');
}
});
function initFauxDevToolsHook() {
const onCommitFiberRoot = jest.fn();
const onCommitFiberUnmount = jest.fn();
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,
};
}
it('can inject the runtime after the renderer executes', async () => {
if (__DEV__) {
initFauxDevToolsHook();
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
ReactFreshRuntime = require('react-refresh/runtime');
ReactFreshRuntime.injectIntoGlobalHook(global);
root = ReactDOMClient.createRoot(container);
await 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;
});
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
await 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;
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
}
});
it('does not block DevTools when an unsupported legacy renderer is injected', () => {
if (__DEV__) {
initFauxDevToolsHook();
const onCommitFiberRoot =
global.__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot;
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'));
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
ReactFreshRuntime = require('react-refresh/runtime');
ReactFreshRuntime.injectIntoGlobalHook(global);
const Hello = () => {
const [state] = React.useState('Hi!');
return React.createElement('div', null, state);
};
$RefreshReg$(Hello, 'Hello');
const Component = Hello;
ReactDOM.render(React.createElement(Component), container);
expect(onCommitFiberRoot).toHaveBeenCalled();
}
});
});