/
githubmr
/
facebook-react
Обзор
Документация
Войти
/
githubmr
/
facebook-react
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
packages/react-refresh/src/__tests__/ReactFresh-test.js
3 835 строк
112 KB
dan
Revert "Fix:- Improve HOC support and state preservation in React Refresh" (#32214)
18 мар 2025, 22:05
Не верифицирован
18 мар 2025, 22:05
86d5ac0
Код
Авторство
О чём код?
/** * 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(); } }); });