Files
react/packages/react-refresh/src/__tests__/ReactFresh-test.js
danandGitHub 96f3093bec Remove RefreshRuntime.findAffectedHostInstances (#30538)
I originally added this with a plan to visualize which nodes got updated
after a Fast Refresh. I didn't end up implementing that part, and to my
knowledge, no actively used integration actually does that or use this
method.

- [Webpack plugin doesn't use
it](https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/f1c8b9a44198449093ca95f85af5df97925e1cfc/lib/runtime/RefreshUtils.js)
- [RN doesn't use
it](https://github.com/facebook/react-native/blob/23c5c42de01953ed7b8e8938c2d3a8fac82250a0/packages/react-native/Libraries/Core/setUpReactRefresh.js)
- [Global GitHub code
search](https://github.com/search?q=findAffectedHostInstances&type=code&p=1)
only shows copies of this code and the type definition based on it, but
not actual calls to it

We should be able to delete this without a problem.
2024-07-30 22:06:16 +01:00

3836 lines
112 KiB
JavaScript

/**
* 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
*/
'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;
}
// 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', 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;
});
// Bump the state before patching.
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');
// Perform a hot update.
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;
});
// 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.
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');
// 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.
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');
// Bump the state again.
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');
// Finally, a render with incompatible type should reset it.
await 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', 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;
});
// Bump the state before patching.
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');
// Perform a hot update.
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;
});
// 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.
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');
// 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.
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');
// Finally, a render with incompatible type should reset it.
await 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', 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 />;
}
// 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');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
// Switching up the inner types should reset the state.
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');
// Switch them up back again.
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');
// Now bump up the state to prepare for patching.
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
// Patch to change the color.
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 />;
}
// 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.
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');
// Now bump up the state to prepare for top-level renders.
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');
// Finally, verify using top-level render with stale type keeps state.
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;
});
// Bump the state before patching.
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');
// Perform a hot update.
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;
});
// 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', 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);
});
// Bump the state before patching.
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');
// Perform a hot update of just the rendering function.
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');
// 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', 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;
});
// Bump the state before patching.
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');
// Perform a hot update.
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;
});
// 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.
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');
// 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.
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');
// Finally, a render with incompatible type should reset it.
await 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', 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;
});
// Bump the state before patching.
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');
// Perform a hot update.
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;
});
// 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.
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');
// 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.
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');
// Finally, a render with incompatible type should reset it.
await 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', 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);
});
// Bump the state before patching.
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');
// Perform a hot update of just the rendering function.
await 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)', 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;
});
// Bump the state before patching.
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');
// Perform a hot update.
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;
});
// 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.
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');
// 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.
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');
// Finally, a render with incompatible type should reset it.
await 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__) {
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');
// Bump the state before patching.
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');
// Perform a hot update.
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;
});
// 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.
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');
// 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.
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');
// Finally, a render with incompatible type should reset it.
await 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__) {
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');
// Perform a hot update.
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();
});
// Expect different color on initial mount.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('red');
// Bump state.
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');
// Test another reload.
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');
// Perform a hot update.
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();
});
// Expect different color on initial mount.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('red');
// Bump state.
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');
// Test another reload.
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');
// Perform a hot update.
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();
});
// Expect different color on initial mount.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('red');
// Bump state.
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');
// Test another reload.
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');
// Perform a hot update.
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();
});
// Expect different color on initial mount.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('red');
// Bump state.
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');
// Test another reload.
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},
);
// 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.
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('');
// Perform a hot update.
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('');
// Now force the tree to suspend.
await 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.
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('');
// Perform a hot update.
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');
});
// Only update color in the visible child
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('');
// Only primary tree should exist now:
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('');
// Perform a hot update.
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);
// Bump the state before patching.
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');
// No re-renders from the top.
expect(appRenders).toBe(1);
// Perform a hot update for Hello only.
await 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.
await 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', 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},
);
// Bump the state before patching.
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');
// Switch the condition, flipping inner content.
// This should reset the state.
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');
// Bump it again.
await act(() => {
el2.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el2.textContent).toBe('1');
// Perform a hot update for both inner components.
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');
});
// 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.
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');
// 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');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
// Perform a hot update.
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');
// 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 = await 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.
await 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.
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');
// Verify we can patch again while preserving the signature.
await 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.
await 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', 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;
});
// 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>,
];
await act(() => {
root.render(null);
});
for (let i = 0; i < trees.length; i++) {
await 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++) {
await act(() => {
root.render(null);
});
// Intentionally don't clean up between the tests:
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');
// 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.
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) => {
// 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.
await 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.
await 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');
});
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 => {
// 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', 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');
// 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');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
// Perform a hot update that doesn't remount.
await 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.
await 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.
await 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.
await 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.
await 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', 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;
});
// 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
await 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.
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;
});
// 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.
await 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', 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');
// Perform a hot update.
await 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 enableLegacyHidden && __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>
);
};
});
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.
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');
});
// 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 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');
// Hot reload while we're offscreen.
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');
});
// 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)', 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;
// Perform a hot update that fails.
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);
// Perform a hot update that fixes the error.
await 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;
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;
// Perform a hot update that fails.
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);
// Perform a hot update that fixes the error.
await 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;
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(); // 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.
let crash;
await patch(() => {
function Hello() {
const [x, setX] = React.useState('');
React.useEffect(() => {
crash = () => {
setX(42); // This will crash next render.
};
}, []);
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:
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);
// Perform a hot update that fixes the error.
await 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;
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('');
// A bad retry
await expect(async () => {
await patch(() => {
function Hello() {
throw new Error('Not yet');
}
$RefreshReg$(Hello, 'Hello');
});
}).rejects.toThrow('Not yet');
expect(container.innerHTML).toBe('');
// Perform a hot update that fixes the error.
await 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.
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>');
// Updates after intentional unmount are ignored.
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('');
// Intentional unmount.
await act(() => {
root.unmount();
});
// Perform a hot update that fixes the error.
await 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', async () => {
if (__DEV__) {
await 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.
await expect(async () => {
await patch(() => {
function Hello() {
throw new Error('No');
}
$RefreshReg$(Hello, 'Hello');
});
}).rejects.toThrow('No');
expect(container.innerHTML).toBe('');
// A bad retry
await expect(async () => {
await patch(() => {
function Hello() {
throw new Error('Not yet');
}
$RefreshReg$(Hello, 'Hello');
});
}).rejects.toThrow('Not yet');
expect(container.innerHTML).toBe('');
// Perform a hot update that fixes the error.
await 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;
await patch(() => {
function Hello() {
return <h1>Nice.</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.firstChild).toBe(helloNode);
expect(helloNode.textContent).toBe('Nice.');
// Break again.
await expect(async () => {
await patch(() => {
function Hello() {
throw new Error('Oops');
}
$RefreshReg$(Hello, 'Hello');
});
}).rejects.toThrow('Oops');
expect(container.innerHTML).toBe('');
// Perform a hot update that fixes the error.
await 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.
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('');
// Mount a new container.
root = ReactDOMClient.createRoot(container);
await render(() => {
function Hello() {
return <h1>Hi</h1>;
}
$RefreshReg$(Hello, 'Hello');
return Hello;
});
expect(container.innerHTML).toBe('<h1>Hi</h1>');
// Break again.
await expect(async () => {
await patch(() => {
function Hello() {
throw new Error('Oops');
}
$RefreshReg$(Hello, 'Hello');
});
}).rejects.toThrow('Oops');
expect(container.innerHTML).toBe('');
// Check we don't attempt to reverse an intentional unmount, even after an error.
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);
// Initially, nothing interesting.
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');
// Then make the first root fail.
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');
// 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');
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>
);
}
}
// 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');
await act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
// Perform a hot update.
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;
});
// 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');
await act(() => {
newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(newEl.textContent).toBe('1');
// Now top-level renders of both types resolve to latest.
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;
});
// 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');
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;
});
// Bump the state before patching.
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');
// Perform a hot update that turns it into a class.
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;
});
// 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');
await act(() => {
newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(newEl.textContent).toBe('1');
// Now top-level renders of both types resolve to latest.
await render(() => HelloV1);
await 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 = 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;
});
// 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');
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');
// Now that it's a function, verify edits keep state.
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__) {
// 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');
await act(() => {
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);
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 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.
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');
// 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');
await act(() => {
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.
await act(() => {
root2.unmount();
});
// 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');
await expect(
act(() => {
ReactFreshRuntime.performReactRefresh();
}),
).rejects.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');
ReactDOMClient = require('react-dom/client');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
// Important! Inject into the global hook *after* ReactDOM runs:
ReactFreshRuntime = require('react-refresh/runtime');
ReactFreshRuntime.injectIntoGlobalHook(global);
root = ReactDOMClient.createRoot(container);
// 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.
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;
});
// Bump the state before patching.
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');
// Perform a hot update.
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;
});
// 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 legacy 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);
// NOTE: Intentionally using createElement in this test instead of JSX
// because old versions of React are incompatible with the JSX transform
// used by our test suite.
const Hello = () => {
const [state] = React.useState('Hi!');
// Intentionally
return React.createElement('div', null, state);
};
$RefreshReg$(Hello, 'Hello');
const Component = Hello;
ReactDOM.render(React.createElement(Component), container);
expect(onCommitFiberRoot).toHaveBeenCalled();
}
});
});