/**
* 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(
setVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); } // 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 (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.forwardRef(() =>setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.forwardRef(() =>setVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); function renderInner() { returnsetVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); function renderInner() { returnsetVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.forwardRef(() =>setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.forwardRef(() =>setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); function renderHello() { returnsetVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); function renderHello() { returnsetVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); } 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 (setVal(val + 1)}> {val}
); } 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 (setVal(val + 1)}> {val}
); } $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 remount when change function to memo', async () => { if (__DEV__) { await act(async () => { await render(() => { function Test() { returnhi test
; } $RefreshReg$(Test, 'Test'); return Test; }); }); // Check the initial render const el = container.firstChild; expect(el.textContent).toBe('hi test'); // Patch to change function to memo await act(async () => { await patch(() => { function Test2() { returnhi memo
; } const Test = React.memo(Test2); $RefreshReg$(Test2, 'Test2'); $RefreshReg$(Test, 'Test'); return Test; }); }); // Check remount expect(container.firstChild).not.toBe(el); const nextEl = container.firstChild; expect(nextEl.textContent).toBe('hi memo'); // Patch back to original function await act(async () => { await patch(() => { function Test() { returnhi test
; } $RefreshReg$(Test, 'Test'); return Test; }); }); // Check final remount expect(container.firstChild).not.toBe(nextEl); const newEl = container.firstChild; expect(newEl.textContent).toBe('hi test'); } }); it('can remount when change memo to forwardRef', async () => { if (__DEV__) { await act(async () => { await render(() => { function Test2() { returnhi memo
; } const Test = React.memo(Test2); $RefreshReg$(Test2, 'Test2'); $RefreshReg$(Test, 'Test'); return Test; }); }); // Check the initial render const el = container.firstChild; expect(el.textContent).toBe('hi memo'); // Patch to change memo to forwardRef await act(async () => { await patch(() => { function Test2() { returnhi forwardRef
; } const Test = React.forwardRef(Test2); $RefreshReg$(Test2, 'Test2'); $RefreshReg$(Test, 'Test'); return Test; }); }); // Check remount expect(container.firstChild).not.toBe(el); const nextEl = container.firstChild; expect(nextEl.textContent).toBe('hi forwardRef'); // Patch back to memo await act(async () => { await patch(() => { function Test2() { returnhi memo
; } const Test = React.memo(Test2); $RefreshReg$(Test2, 'Test2'); $RefreshReg$(Test, 'Test'); return Test; }); }); // Check final remount expect(container.firstChild).not.toBe(nextEl); const newEl = container.firstChild; expect(newEl.textContent).toBe('hi memo'); } }); it('can remount when change function to forwardRef', async () => { if (__DEV__) { await act(async () => { await render(() => { function Test() { returnhi test
; } $RefreshReg$(Test, 'Test'); return Test; }); }); // Check the initial render const el = container.firstChild; expect(el.textContent).toBe('hi test'); // Patch to change function to forwardRef await act(async () => { await patch(() => { function Test2() { returnhi forwardRef
; } const Test = React.forwardRef(Test2); $RefreshReg$(Test2, 'Test2'); $RefreshReg$(Test, 'Test'); return Test; }); }); // Check remount expect(container.firstChild).not.toBe(el); const nextEl = container.firstChild; expect(nextEl.textContent).toBe('hi forwardRef'); // Patch back to a new function await act(async () => { await patch(() => { function Test() { returnhi test1
; } $RefreshReg$(Test, 'Test'); return Test; }); }); // Check final remount expect(container.firstChild).not.toBe(nextEl); const newEl = container.firstChild; expect(newEl.textContent).toBe('hi test1'); } }); it('can remount when change memo inner type from function to forwardRef', async () => { if (__DEV__) { await act(async () => { await render(() => { function Test2() { returnhi memo
; } const Test = React.memo(Test2); $RefreshReg$(Test2, 'Test$React.memo'); $RefreshReg$(Test, 'Test'); return Test; }); }); // Check the initial render const el = container.firstChild; expect(el.textContent).toBe('hi memo'); // Patch to wrap the inner function in forwardRef. // The outer type is still a memo, so only the inner family changes. await act(async () => { await patch(() => { function Test2(props, ref) { returnhi memo forwardRef
; } const Test2Ref = React.forwardRef(Test2); const Test = React.memo(Test2Ref); $RefreshReg$(Test2, 'Test$React.memo$React.forwardRef'); $RefreshReg$(Test2Ref, 'Test$React.memo'); $RefreshReg$(Test, 'Test'); return Test; }); }); // Check remount expect(container.firstChild).not.toBe(el); const nextEl = container.firstChild; expect(nextEl.textContent).toBe('hi memo forwardRef'); // Patch back to a plain function inside memo await act(async () => { await patch(() => { function Test2() { returnhi memo
; } const Test = React.memo(Test2); $RefreshReg$(Test2, 'Test$React.memo'); $RefreshReg$(Test, 'Test'); return Test; }); }); // Check final remount expect(container.firstChild).not.toBe(nextEl); const newEl = container.firstChild; expect(newEl.textContent).toBe('hi memo'); } }); it('can mount an element created before its type changed kinds', async () => { if (__DEV__) { let oldElement; let currentChild = null; await act(async () => { await render(() => { function Test() { returnhi test
; } $RefreshReg$(Test, 'Test'); oldElement =hi memo
; } const Test = React.memo(Test2); $RefreshReg$(Test2, 'Test$React.memo'); $RefreshReg$(Test, 'Test'); function App() { const [, forceUpdate] = React.useState(0); return (hi memo
; } const Test = React.memo(Test2); $RefreshReg$(Test2, 'Test$React.memo'); $RefreshReg$(Test, 'Test'); return Test; }); }); // Check the initial render const el = container.firstChild; expect(el.textContent).toBe('hi memo'); // Patch to add a custom comparison function. // The fiber can no longer be a SimpleMemoComponent. await act(async () => { await patch(() => { function Test2() { returnhi memo with compare
; } const Test = React.memo(Test2, (prevProps, nextProps) => false); $RefreshReg$(Test2, 'Test$React.memo'); $RefreshReg$(Test, 'Test'); return Test; }); }); // Check remount expect(container.firstChild).not.toBe(el); const nextEl = container.firstChild; expect(nextEl.textContent).toBe('hi memo with compare'); // Patch to remove the comparison function again await act(async () => { await patch(() => { function Test2() { returnhi memo
; } const Test = React.memo(Test2); $RefreshReg$(Test2, 'Test$React.memo'); $RefreshReg$(Test, 'Test'); return Test; }); }); // Check final remount expect(container.firstChild).not.toBe(nextEl); const newEl = container.firstChild; expect(newEl.textContent).toBe('hi memo'); } }); it('can update a memo comparison function in place', async () => { if (__DEV__) { await act(async () => { await render(() => { function Inner({label}) { return{label}
; } const InnerMemo = React.memo(Inner, (prevProps, nextProps) => true); $RefreshReg$(Inner, 'Inner$React.memo'); $RefreshReg$(InnerMemo, 'Inner'); function App() { const [n, setN] = React.useState(1); return ({label}
; } const InnerMemo = React.memo(Inner, (prevProps, nextProps) => false); $RefreshReg$(Inner, 'Inner$React.memo'); $RefreshReg$(InnerMemo, 'Inner'); function App() { const [n, setN] = React.useState(1); return ({label}
; } const InnerMemo = React.memo(Inner); $RefreshReg$(Inner, 'Inner$React.memo'); $RefreshReg$(InnerMemo, 'Inner'); oldElement ={label}
; } const InnerMemo = React.memo(Inner, (prevProps, nextProps) => true); $RefreshReg$(Inner, 'Inner$React.memo'); $RefreshReg$(InnerMemo, 'Inner'); newElement =hi memo
; } const Inner = React.memo(Hello); $RefreshReg$(Hello, 'Hello'); $RefreshReg$(Inner, 'Inner'); const Outer = React.lazy( () => new Promise(_resolve => { resolve = () => _resolve({default: Inner}); }), ); $RefreshReg$(Outer, 'Outer'); function App() { return (hi memo with compare
; } const Inner = React.memo(Hello, (prevProps, nextProps) => false); $RefreshReg$(Hello, 'Hello'); $RefreshReg$(Inner, 'Inner'); const Outer = React.lazy( () => new Promise(_resolve => { resolve = () => _resolve({default: Inner}); }), ); $RefreshReg$(Outer, 'Outer'); function App() { return (hi memo
; } const Inner = React.memo(Hello); $RefreshReg$(Hello, 'Hello'); $RefreshReg$(Inner, 'Inner'); const Outer = React.lazy( () => new Promise(_resolve => { resolve = () => _resolve({default: Inner}); }), ); $RefreshReg$(Outer, 'Outer'); function App() { return (hi memo with compare
; } const Inner = React.memo(Hello, (prevProps, nextProps) => false); $RefreshReg$(Hello, 'Hello'); $RefreshReg$(Inner, 'Inner'); return Inner; }); // The remount goes through the old lazy, whose payload is already // resolved, so it doesn't suspend. expect(container.textContent).toBe('hi memo with compare'); expect(container.firstChild).not.toBe(el); } }); it('can remount an unregistered memo wrapper without losing the wrapper', async () => { if (__DEV__) { let innerRenders = 0; await act(async () => { await render(() => { function Inner({label}) { innerRenders++; return{label}
; } $RefreshReg$(Inner, 'Inner'); $RefreshSig$(Inner, 'sig1'); // The wrapper is deliberately not registered, like a wrapper // created inside a third-party HOC. const InnerMemo = React.memo(Inner); function App() { const [, forceUpdate] = React.useState(0); return ({label}
; } $RefreshReg$(Inner, 'Inner'); $RefreshSig$(Inner, 'sig2'); return Inner; }); }); expect(innerRenders).toBe(2); const innerEl = container.firstChild.firstChild; // The remounted fiber must still be a memo: equal props stay // blocked, and the fiber reconciles against the original element // instead of being replaced again. await act(async () => { container.firstChild.click(); }); expect(innerRenders).toBe(2); expect(container.firstChild.firstChild).toBe(innerEl); } }); it('resets state when switching between different component types', async () => { if (__DEV__) { await act(async () => { await render(() => { function Test() { const [count, setCount] = React.useState(0); return (setVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.memo(React.forwardRef(() =>setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.memo(React.forwardRef(() =>setVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.lazy( () => new Promise(_resolve => { resolve = () => _resolve({default: Hello}); }), ); $RefreshReg$(Outer, 'Outer'); function App() { return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.lazy( () => new Promise(_resolve => { resolve = () => _resolve({default: Hello}); }), ); $RefreshReg$(Outer, 'Outer'); function App() { return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); // Note: no lazy wrapper this time. function App() { return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.lazy( () => new Promise(_resolve => { resolve = () => _resolve({default: Hello}); }), ); $RefreshReg$(Outer, 'Outer'); function App() { return (setVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); } 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 (setVal(val + 1)}> {val}
); } 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 (setVal(val + 1)}> {val}
); } 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 (setVal(val + 1)}> {val}
); } 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 (setVal(val + 1)}> {val}
); } 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 (setVal(val + 1)}> {val}
); } 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 (setVal(val + 1)}> {val}
); } 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 (setVal(val + 1)}> {val}
); } 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 (setVal(val + 1)}> {val}
); } 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 (setVal(val + 1)}> {children} {val}
); } $RefreshReg$(Hello, 'Hello'); function Never() { throw new Promise(resolve => {}); } function App({shouldSuspend}) { return (setVal(val + 1)}> {children} {val}
); } $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 (setVal(val + 1)}> {children} {val}
); } $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 (setVal(val + 1)}> {children} {val}
); } $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 (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); function App() { appRenders++; returnsetVal(val + 1)}> {val}
); } $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++; returnsetVal(val + 1)}> {val}
); } $RefreshReg$(Hello1, 'Hello1'); function Hello2() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello2, 'Hello2'); function App({cond}) { return cond ?setVal(val + 1)}> {val}
); } $RefreshReg$(Hello1, 'Hello1'); function Hello2() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); } // 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 (setVal(val + 1)}> {val}
); } // 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 (setVal(val + 1)}> {val}
); } // 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 = [setVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); } // 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 (setVal(val + 1)}> {val}
); } // 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 (setVal(val + 1)}> {val}
); } // 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('double invokes effects after a forced remount in StrictMode', async () => { if (__DEV__) { const log = []; const createAppV1 = () => { function Hello() { React.useEffect(() => { log.push('mount v1'); return () => log.push('unmount v1'); }, []); returnHello
; } $RefreshReg$(Hello, 'Hello'); $RefreshSig$(Hello, '1'); return Hello; }; const App = createAppV1(); await act(() => { root.render(Hello
; } $RefreshReg$(Hello, 'Hello'); $RefreshSig$(Hello, '2'); return null; }); expect(container.firstChild.style.color).toBe('red'); expect(log).toEqual(['unmount v1', 'mount v2', 'unmount v2', 'mount v2']); } }); it('double invokes an effect added during Fast Refresh remount in StrictMode', async () => { if (__DEV__) { const log = []; const createAppV1 = () => { function Hello() { returnHello
; } $RefreshReg$(Hello, 'Hello'); $RefreshSig$(Hello, '1'); return Hello; }; const App = createAppV1(); await act(() => { root.render(Hello
; } $RefreshReg$(Hello, 'Hello'); $RefreshSig$(Hello, '2'); return null; }); expect(container.firstChild.style.color).toBe('red'); expect(log).toEqual(['mount v2', 'unmount v2', 'mount v2']); } }); 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 ({tranformed}
); } $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 ({tranformed}
); } $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{state.value}
; } $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{state.value}
; } $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 (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); return function App({offscreen}) { React.useLayoutEffect(() => { Scheduler.log('App#layout'); }); return (setVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); } $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() { returnA
B
> ); } return App; }); expect(container.innerHTML).toBe('A
B
'); 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('A
B
'); expect(container.firstChild).toBe(firstP); expect(container.firstChild.nextSibling.nextSibling).toBe(secondP); // Perform a hot update that fixes the error. await patch(() => { function Hello() { returnA
B
'); 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() { returnA
B
> ); } return App; }); expect(container.innerHTML).toBe('A
B
'); 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('A
B
'); expect(container.firstChild).toBe(firstP); expect(container.firstChild.nextSibling.nextSibling).toBe(secondP); // Perform a hot update that fixes the error. await patch(() => { function Hello() { returnA
B
'); 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() { returnA
B
> ); } return App; }); expect(container.innerHTML).toBe('A
B
'); 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(); returnA
B
'); // Run timeout inside effect: await act(() => { crash(); }); expect(container.innerHTML).toBe( 'A
B
', ); 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. returnA
B
'); 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{this.state.count}
); } } // 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 ({this.state.count}
); } } $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 ({this.state.count}
); } } $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 ; } } $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 ; } } $RefreshReg$(Hello, 'Hello'); }); expect(testRef.current.getColor()).toBe('orange'); await patch(() => { const Hello = React.forwardRef((props, ref) => { React.useImperativeHandle(ref, () => ({ getColor() { return 'pink'; }, })); return ; }); $RefreshReg$(Hello, 'Hello'); }); expect(testRef.current.getColor()).toBe('pink'); await patch(() => { const Hello = React.forwardRef((props, ref) => { React.useImperativeHandle(ref, () => ({ getColor() { return 'yellow'; }, })); return ; }); $RefreshReg$(Hello, 'Hello'); }); expect(testRef.current.getColor()).toBe('yellow'); await patch(() => { const Hello = React.forwardRef((props, ref) => { React.useImperativeHandle(ref, () => ({ getColor() { return 'yellow'; }, })); return ; }); $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 (setVal(val + 1)}> {val}
); } $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 ({this.state.count}
); } } $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 (setVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); }; $RefreshReg$(HelloV1, 'Hello'); // Perform a hot update before any roots exist. const HelloV2 = () => { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); }; $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(setVal(val + 1)}> {val}
); }; $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 (setVal(val + 1)}> {val}
); }; $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 (setVal(val + 1)}> {val}
); }; $RefreshReg$(HelloV1, 'Hello'); const HelloV2 = () => { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); }; $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 (setVal(val + 1)}> {val}
); } $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 (setVal(val + 1)}> {val}
); } $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(); } }); });