diff --git a/packages/react-dom/src/__tests__/ReactFresh-test.internal.js b/packages/react-dom/src/__tests__/ReactFresh-test.internal.js
new file mode 100644
index 0000000000..4aa0d30bfa
--- /dev/null
+++ b/packages/react-dom/src/__tests__/ReactFresh-test.internal.js
@@ -0,0 +1,2036 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+/* eslint-disable no-for-of-loops/no-for-of-loops */
+
+'use strict';
+
+let React;
+let ReactDOM;
+let Scheduler;
+let act;
+
+describe('ReactFresh', () => {
+ let container;
+ let familiesByID;
+ let familiesByType;
+ let newFamilies;
+ let updatedFamilies;
+ let performHotReload;
+ let signaturesByType;
+
+ beforeEach(() => {
+ let scheduleHotUpdate;
+ let lastRoot;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
+ supportsFiber: true,
+ inject: injected => {
+ scheduleHotUpdate = injected.scheduleHotUpdate;
+ },
+ onCommitFiberRoot: (id, root) => {
+ lastRoot = root;
+ },
+ onCommitFiberUnmount: () => {},
+ };
+
+ jest.resetModules();
+ React = require('react');
+ ReactDOM = require('react-dom');
+ Scheduler = require('scheduler');
+ act = require('react-dom/test-utils').act;
+ container = document.createElement('div');
+ document.body.appendChild(container);
+
+ familiesByID = new Map();
+ familiesByType = new WeakMap();
+
+ if (__DEV__) {
+ performHotReload = function(staleFamilies) {
+ scheduleHotUpdate({
+ root: lastRoot,
+ familiesByType,
+ updatedFamilies,
+ staleFamilies,
+ });
+ };
+ }
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ });
+
+ function prepare(version) {
+ newFamilies = new Set();
+ updatedFamilies = new Set();
+ signaturesByType = new Map();
+ const Component = version();
+
+ // Fill in the signatures.
+ for (let family of newFamilies) {
+ const latestSignature = signaturesByType.get(family.currentType) || null;
+ family.currentSignature = latestSignature;
+ }
+
+ newFamilies = null;
+ updatedFamilies = null;
+ signaturesByType = null;
+
+ return Component;
+ }
+
+ function render(version, props) {
+ const Component = prepare(version);
+ act(() => {
+ ReactDOM.render(, container);
+ });
+ return Component;
+ }
+
+ function patch(version) {
+ // Will be filled in by __register__ calls in user code.
+ newFamilies = new Set();
+ updatedFamilies = new Set();
+ signaturesByType = new Map();
+ const Component = version();
+
+ // Fill in the signatures.
+ for (let family of newFamilies) {
+ const latestSignature = signaturesByType.get(family.currentType) || null;
+ family.currentSignature = latestSignature;
+ }
+ // Now that all registration and signatures are collected,
+ // find which registrations changed their signatures since last time.
+ const staleFamilies = new Set();
+ for (let family of updatedFamilies) {
+ const latestSignature = signaturesByType.get(family.currentType) || null;
+ if (family.currentSignature !== latestSignature) {
+ family.currentSignature = latestSignature;
+ staleFamilies.add(family);
+ }
+ }
+
+ performHotReload(staleFamilies);
+ newFamilies = null;
+ updatedFamilies = null;
+ signaturesByType = null;
+ return Component;
+ }
+
+ function __register__(type, id) {
+ if (familiesByType.has(type)) {
+ return;
+ }
+ let family = familiesByID.get(id);
+ let isNew = false;
+ if (family === undefined) {
+ isNew = true;
+ family = {currentType: type, currentSignature: null};
+ familiesByID.set(id, family);
+ }
+ const prevType = family.currentType;
+ if (isNew) {
+ // The first time a type is registered, we don't need
+ // any special reconciliation logic. So we won't add it to the map.
+ // Instead, this will happen the firt time it is edited.
+ newFamilies.add(family);
+ } else {
+ family.currentType = type;
+ // Point both previous and next types to this family.
+ familiesByType.set(prevType, family);
+ familiesByType.set(type, family);
+ updatedFamilies.add(family);
+ }
+
+ if (typeof type === 'object' && type !== null) {
+ switch (type.$$typeof) {
+ case Symbol.for('react.forward_ref'):
+ __register__(type.render, id + '$render');
+ break;
+ case Symbol.for('react.memo'):
+ __register__(type.type, id + '$type');
+ break;
+ }
+ }
+ }
+
+ function __signature__(type, signature) {
+ signaturesByType.set(type, signature);
+ }
+
+ it('can preserve state for compatible types', () => {
+ if (__DEV__) {
+ let HelloV1 = render(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+
setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+ return Hello;
+ });
+
+ // Bump the state before patching.
+ const el = container.firstChild;
+ expect(el.textContent).toBe('0');
+ expect(el.style.color).toBe('blue');
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(el.textContent).toBe('1');
+
+ // Perform a hot update.
+ let HelloV2 = patch(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+ return Hello;
+ });
+
+ // Assert the state was preserved but color changed.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('1');
+ expect(el.style.color).toBe('red');
+
+ // Bump the state again.
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('2');
+ expect(el.style.color).toBe('red');
+
+ // Perform top-down renders with both fresh and stale types.
+ // Neither should change the state or color.
+ // They should always resolve to the latest version.
+ render(() => HelloV1);
+ render(() => HelloV2);
+ render(() => HelloV1);
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('2');
+ expect(el.style.color).toBe('red');
+
+ // Bump the state again.
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('3');
+ expect(el.style.color).toBe('red');
+
+ // Finally, a render with incompatible type should reset it.
+ render(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ 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', () => {
+ if (__DEV__) {
+ let OuterV1 = render(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+
+ const Outer = React.forwardRef(() => );
+ __register__(Outer, 'Outer');
+ return Outer;
+ });
+
+ // Bump the state before patching.
+ const el = container.firstChild;
+ expect(el.textContent).toBe('0');
+ expect(el.style.color).toBe('blue');
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(el.textContent).toBe('1');
+
+ // Perform a hot update.
+ let OuterV2 = patch(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+
+ const Outer = React.forwardRef(() => );
+ __register__(Outer, 'Outer');
+ return Outer;
+ });
+
+ // Assert the state was preserved but color changed.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('1');
+ expect(el.style.color).toBe('red');
+
+ // Bump the state again.
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('2');
+ expect(el.style.color).toBe('red');
+
+ // Perform top-down renders with both fresh and stale types.
+ // Neither should change the state or color.
+ // They should always resolve to the latest version.
+ render(() => OuterV1);
+ render(() => OuterV2);
+ render(() => OuterV1);
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('2');
+ expect(el.style.color).toBe('red');
+
+ // Finally, a render with incompatible type should reset it.
+ render(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+
+ // Note: no forwardRef wrapper this time.
+ return Hello;
+ });
+
+ expect(container.firstChild).not.toBe(el);
+ const newEl = container.firstChild;
+ expect(newEl.textContent).toBe('0');
+ expect(newEl.style.color).toBe('blue');
+ }
+ });
+
+ it('should not consider two forwardRefs around the same type to be equivalent', () => {
+ if (__DEV__) {
+ let ParentV1 = render(
+ () => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+
+ function renderInner() {
+ return ;
+ }
+ // Both of these are wrappers around the same inner function.
+ // They should be treated as distinct types across reloads.
+ let ForwardRefA = React.forwardRef(renderInner);
+ __register__(ForwardRefA, 'ForwardRefA');
+ let ForwardRefB = React.forwardRef(renderInner);
+ __register__(ForwardRefB, 'ForwardRefB');
+
+ function Parent({cond}) {
+ return cond ? : ;
+ }
+ __register__(Parent, 'Parent');
+
+ return Parent;
+ },
+ {cond: true},
+ );
+
+ // Bump the state before switching up types.
+ let el = container.firstChild;
+ expect(el.textContent).toBe('0');
+ expect(el.style.color).toBe('blue');
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(el.textContent).toBe('1');
+
+ // Switching up the inner types should reset the state.
+ render(() => ParentV1, {cond: false});
+ expect(el).not.toBe(container.firstChild);
+ el = container.firstChild;
+ expect(el.textContent).toBe('0');
+ expect(el.style.color).toBe('blue');
+
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(el.textContent).toBe('1');
+
+ // Switch them up back again.
+ render(() => ParentV1, {cond: true});
+ expect(el).not.toBe(container.firstChild);
+ el = container.firstChild;
+ expect(el.textContent).toBe('0');
+ expect(el.style.color).toBe('blue');
+
+ // Now bump up the state to prepare for patching.
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(el.textContent).toBe('1');
+
+ // Patch to change the color.
+ let ParentV2 = patch(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+
+ function renderInner() {
+ return ;
+ }
+ // Both of these are wrappers around the same inner function.
+ // They should be treated as distinct types across reloads.
+ let ForwardRefA = React.forwardRef(renderInner);
+ __register__(ForwardRefA, 'ForwardRefA');
+ let ForwardRefB = React.forwardRef(renderInner);
+ __register__(ForwardRefB, 'ForwardRefB');
+
+ function Parent({cond}) {
+ return cond ? : ;
+ }
+ __register__(Parent, 'Parent');
+
+ return Parent;
+ });
+
+ // The state should be intact; the color should change.
+ expect(el).toBe(container.firstChild);
+ expect(el.textContent).toBe('1');
+ expect(el.style.color).toBe('red');
+
+ // Switching up the condition should still reset the state.
+ render(() => ParentV2, {cond: false});
+ expect(el).not.toBe(container.firstChild);
+ el = container.firstChild;
+ expect(el.textContent).toBe('0');
+ expect(el.style.color).toBe('red');
+
+ // Now bump up the state to prepare for top-level renders.
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(el).toBe(container.firstChild);
+ expect(el.textContent).toBe('1');
+ expect(el.style.color).toBe('red');
+
+ // Finally, verify using top-level render with stale type keeps state.
+ render(() => ParentV1);
+ render(() => ParentV2);
+ render(() => ParentV1);
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('1');
+ expect(el.style.color).toBe('red');
+ }
+ });
+
+ it('can update forwardRef render function with its wrapper', () => {
+ if (__DEV__) {
+ render(() => {
+ function Hello({color}) {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+
+ const Outer = React.forwardRef(() => );
+ __register__(Outer, 'Outer');
+ return Outer;
+ });
+
+ // Bump the state before patching.
+ const el = container.firstChild;
+ expect(el.textContent).toBe('0');
+ expect(el.style.color).toBe('blue');
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(el.textContent).toBe('1');
+
+ // Perform a hot update.
+ patch(() => {
+ function Hello({color}) {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+
+ const Outer = React.forwardRef(() => );
+ __register__(Outer, 'Outer');
+ return Outer;
+ });
+
+ // Assert the state was preserved but color changed.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('1');
+ expect(el.style.color).toBe('red');
+ }
+ });
+
+ it('can update forwardRef render function in isolation', () => {
+ if (__DEV__) {
+ render(() => {
+ function Hello({color}) {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+
+ function renderHello() {
+ return ;
+ }
+ __register__(renderHello, 'renderHello');
+
+ return React.forwardRef(renderHello);
+ });
+
+ // Bump the state before patching.
+ const el = container.firstChild;
+ expect(el.textContent).toBe('0');
+ expect(el.style.color).toBe('blue');
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(el.textContent).toBe('1');
+
+ // Perform a hot update of just the rendering function.
+ patch(() => {
+ function Hello({color}) {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+
+ function renderHello() {
+ return ;
+ }
+ __register__(renderHello, 'renderHello');
+
+ // Not updating the wrapper.
+ });
+
+ // Assert the state was preserved but color changed.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('1');
+ expect(el.style.color).toBe('red');
+ }
+ });
+
+ it('can preserve state for simple memo', () => {
+ if (__DEV__) {
+ let OuterV1 = render(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+
+ const Outer = React.memo(Hello);
+ __register__(Outer, 'Outer');
+ return Outer;
+ });
+
+ // Bump the state before patching.
+ const el = container.firstChild;
+ expect(el.textContent).toBe('0');
+ expect(el.style.color).toBe('blue');
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(el.textContent).toBe('1');
+
+ // Perform a hot update.
+ let OuterV2 = patch(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+
+ const Outer = React.memo(Hello);
+ __register__(Outer, 'Outer');
+ return Outer;
+ });
+
+ // Assert the state was preserved but color changed.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('1');
+ expect(el.style.color).toBe('red');
+
+ // Bump the state again.
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('2');
+ expect(el.style.color).toBe('red');
+
+ // Perform top-down renders with both fresh and stale types.
+ // Neither should change the state or color.
+ // They should always resolve to the latest version.
+ render(() => OuterV1);
+ render(() => OuterV2);
+ render(() => OuterV1);
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('2');
+ expect(el.style.color).toBe('red');
+
+ // Finally, a render with incompatible type should reset it.
+ render(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+
+ // Note: no wrapper this time.
+ return Hello;
+ });
+
+ expect(container.firstChild).not.toBe(el);
+ const newEl = container.firstChild;
+ expect(newEl.textContent).toBe('0');
+ expect(newEl.style.color).toBe('blue');
+ }
+ });
+
+ it('can preserve state for memo with custom comparison', () => {
+ if (__DEV__) {
+ let OuterV1 = render(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+
+ const Outer = React.memo(Hello, () => true);
+ __register__(Outer, 'Outer');
+ return Outer;
+ });
+
+ // Bump the state before patching.
+ const el = container.firstChild;
+ expect(el.textContent).toBe('0');
+ expect(el.style.color).toBe('blue');
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(el.textContent).toBe('1');
+
+ // Perform a hot update.
+ let OuterV2 = patch(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+
+ const Outer = React.memo(Hello, () => true);
+ __register__(Outer, 'Outer');
+ return Outer;
+ });
+
+ // Assert the state was preserved but color changed.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('1');
+ expect(el.style.color).toBe('red');
+
+ // Bump the state again.
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('2');
+ expect(el.style.color).toBe('red');
+
+ // Perform top-down renders with both fresh and stale types.
+ // Neither should change the state or color.
+ // They should always resolve to the latest version.
+ render(() => OuterV1);
+ render(() => OuterV2);
+ render(() => OuterV1);
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('2');
+ expect(el.style.color).toBe('red');
+
+ // Finally, a render with incompatible type should reset it.
+ render(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+
+ // Note: no wrapper this time.
+ return Hello;
+ });
+
+ expect(container.firstChild).not.toBe(el);
+ const newEl = container.firstChild;
+ expect(newEl.textContent).toBe('0');
+ expect(newEl.style.color).toBe('blue');
+ }
+ });
+
+ it('can update simple memo function in isolation', () => {
+ if (__DEV__) {
+ render(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+
+ return React.memo(Hello);
+ });
+
+ // Bump the state before patching.
+ const el = container.firstChild;
+ expect(el.textContent).toBe('0');
+ expect(el.style.color).toBe('blue');
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(el.textContent).toBe('1');
+
+ // Perform a hot update of just the rendering function.
+ patch(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+
+ // Not updating the wrapper.
+ });
+
+ // Assert the state was preserved but color changed.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('1');
+ expect(el.style.color).toBe('red');
+ }
+ });
+
+ it('can preserve state for memo(forwardRef)', () => {
+ if (__DEV__) {
+ let OuterV1 = render(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+
+ const Outer = React.memo(React.forwardRef(() => ));
+ __register__(Outer, 'Outer');
+ return Outer;
+ });
+
+ // Bump the state before patching.
+ const el = container.firstChild;
+ expect(el.textContent).toBe('0');
+ expect(el.style.color).toBe('blue');
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(el.textContent).toBe('1');
+
+ // Perform a hot update.
+ let OuterV2 = patch(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+
+ const Outer = React.memo(React.forwardRef(() => ));
+ __register__(Outer, 'Outer');
+ return Outer;
+ });
+
+ // Assert the state was preserved but color changed.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('1');
+ expect(el.style.color).toBe('red');
+
+ // Bump the state again.
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('2');
+ expect(el.style.color).toBe('red');
+
+ // Perform top-down renders with both fresh and stale types.
+ // Neither should change the state or color.
+ // They should always resolve to the latest version.
+ render(() => OuterV1);
+ render(() => OuterV2);
+ render(() => OuterV1);
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('2');
+ expect(el.style.color).toBe('red');
+
+ // Finally, a render with incompatible type should reset it.
+ render(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(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('does not re-render ancestor components unnecessarily during a hot update', () => {
+ if (__DEV__) {
+ let appRenders = 0;
+
+ render(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+ function App() {
+ appRenders++;
+ return ;
+ }
+ __register__(App, 'App');
+ return App;
+ });
+
+ expect(appRenders).toBe(1);
+
+ // Bump the state before patching.
+ const el = container.firstChild;
+ expect(el.textContent).toBe('0');
+ expect(el.style.color).toBe('blue');
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(el.textContent).toBe('1');
+
+ // No re-renders from the top.
+ expect(appRenders).toBe(1);
+
+ // Perform a hot update for Hello only.
+ patch(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+ });
+
+ // Assert the state was preserved but color changed.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('1');
+ expect(el.style.color).toBe('red');
+
+ // Still no re-renders from the top.
+ expect(appRenders).toBe(1);
+
+ // Bump the state.
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(el.textContent).toBe('2');
+
+ // Still no re-renders from the top.
+ expect(appRenders).toBe(1);
+ }
+ });
+
+ it('can force remount by changing signature', () => {
+ if (__DEV__) {
+ let HelloV1 = render(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+ // When this changes, we'll expect a remount:
+ __signature__(Hello, '1');
+ return Hello;
+ });
+
+ // Bump the state before patching.
+ const el = container.firstChild;
+ expect(el.textContent).toBe('0');
+ expect(el.style.color).toBe('blue');
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(el.textContent).toBe('1');
+
+ // Perform a hot update.
+ let HelloV2 = patch(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+ // The signature hasn't changed since the last time:
+ __signature__(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.
+ let HelloV3 = patch(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ // We're changing the signature now so it will remount:
+ __register__(Hello, 'Hello');
+ __signature__(Hello, '2');
+ return Hello;
+ });
+
+ // Expect a remount.
+ expect(container.firstChild).not.toBe(el);
+ const newEl = container.firstChild;
+ expect(newEl.textContent).toBe('0');
+ expect(newEl.style.color).toBe('yellow');
+
+ // Bump state again.
+ act(() => {
+ newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(newEl.textContent).toBe('1');
+ expect(newEl.style.color).toBe('yellow');
+
+ // Perform top-down renders with both fresh and stale types.
+ // Neither should change the state or color.
+ // They should always resolve to the latest version.
+ render(() => HelloV1);
+ render(() => HelloV2);
+ render(() => HelloV3);
+ render(() => HelloV2);
+ render(() => HelloV1);
+ expect(container.firstChild).toBe(newEl);
+ expect(newEl.textContent).toBe('1');
+ expect(newEl.style.color).toBe('yellow');
+
+ // Verify we can patch again while preserving the signature.
+ patch(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ // Same signature as last time.
+ __register__(Hello, 'Hello');
+ __signature__(Hello, '2');
+ return Hello;
+ });
+
+ expect(container.firstChild).toBe(newEl);
+ expect(newEl.textContent).toBe('1');
+ expect(newEl.style.color).toBe('purple');
+
+ // Check removing the signature also causes a remount.
+ patch(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ // No signature this time.
+ __register__(Hello, 'Hello');
+ return Hello;
+ });
+
+ // Expect a remount.
+ expect(container.firstChild).not.toBe(newEl);
+ const finalEl = container.firstChild;
+ expect(finalEl.textContent).toBe('0');
+ expect(finalEl.style.color).toBe('orange');
+ }
+ });
+
+ it('keeps a valid tree when forcing remount', () => {
+ if (__DEV__) {
+ let HelloV1 = prepare(() => {
+ function Hello() {
+ return null;
+ }
+ __register__(Hello, 'Hello');
+ __signature__(Hello, '1');
+ return Hello;
+ });
+
+ const Bailout = React.memo(({children}) => {
+ return children;
+ });
+
+ // Each of those renders three instances of HelloV1,
+ // but in different ways.
+ let trees = [
+ ,
+ ,
+
+
+
+
+
+
,
+
+
+
+
+
+
,
+ ,
+
+
+
+ Hello
+
+ ,
+
+
+
+
+ ,
+
,
+
+
+
+
+
+
+
+
+
+
+ ,
+
+
+
+
+
+
+
,
+
+
+ {null}
+
+ {null}
+
+
+
+
,
+
+
+
+
+
+
+
,
+
+ {[[]]}
+
+
+ {[null]}
+
+
+
,
+
+ {['foo', , null, ]}
+
+ {[null]}
+
+
+
,
+
+
+
+
+ hi
+
+
+
+ ,
+ ];
+
+ // First, check that each tree handles remounts in isolation.
+ ReactDOM.render(null, container);
+ for (let i = 0; i < trees.length; i++) {
+ runRemountingStressTest(trees[i]);
+ }
+
+ // Then check that each tree is resilient to updates from another tree.
+ for (let i = 0; i < trees.length; i++) {
+ for (let j = 0; j < trees.length; j++) {
+ ReactDOM.render(null, container);
+ // Intentionally don't clean up between the tests:
+ runRemountingStressTest(trees[i]);
+ runRemountingStressTest(trees[j]);
+ runRemountingStressTest(trees[i]);
+ }
+ }
+ }
+ });
+
+ function runRemountingStressTest(tree) {
+ patch(() => {
+ function Hello({children}) {
+ return ;
+ }
+ __register__(Hello, 'Hello');
+ __signature__(Hello, '1');
+ return Hello;
+ });
+
+ ReactDOM.render(tree, container);
+ const elements = container.querySelectorAll('section');
+ // Each tree above products exactly three elements:
+ expect(elements.length).toBe(3);
+ elements.forEach(el => {
+ expect(el.dataset.color).toBe('blue');
+ });
+
+ // Patch color without changing the signature.
+ patch(() => {
+ function Hello({children}) {
+ return ;
+ }
+ __register__(Hello, 'Hello');
+ __signature__(Hello, '1');
+ return Hello;
+ });
+
+ const elementsAfterPatch = container.querySelectorAll('section');
+ expect(elementsAfterPatch.length).toBe(3);
+ elementsAfterPatch.forEach((el, index) => {
+ // The signature hasn't changed so we expect DOM nodes to stay the same.
+ expect(el).toBe(elements[index]);
+ // However, the color should have changed:
+ expect(el.dataset.color).toBe('red');
+ });
+
+ // Patch color *and* change the signature.
+ patch(() => {
+ function Hello({children}) {
+ return ;
+ }
+ __register__(Hello, 'Hello');
+ __signature__(Hello, '2'); // Remount
+ return Hello;
+ });
+
+ const elementsAfterRemount = container.querySelectorAll('section');
+ expect(elementsAfterRemount.length).toBe(3);
+ elementsAfterRemount.forEach((el, index) => {
+ // The signature changed so we expect DOM nodes to be different.
+ expect(el).not.toBe(elements[index]);
+ // They should all be using the new color:
+ expect(el.dataset.color).toBe('orange');
+ });
+
+ // Now patch color but *don't* change the signature.
+ patch(() => {
+ function Hello({children}) {
+ return ;
+ }
+ __register__(Hello, 'Hello');
+ __signature__(Hello, '2'); // Same signature as before
+ return Hello;
+ });
+
+ expect(container.querySelectorAll('section').length).toBe(3);
+ container.querySelectorAll('section').forEach((el, index) => {
+ // The signature didn't change so DOM nodes should stay the same.
+ expect(el).toBe(elementsAfterRemount[index]);
+ // They should all be using the new color:
+ expect(el.dataset.color).toBe('black');
+ });
+
+ // Do another render just in case.
+ ReactDOM.render(tree, container);
+ expect(container.querySelectorAll('section').length).toBe(3);
+ container.querySelectorAll('section').forEach((el, index) => {
+ expect(el).toBe(elementsAfterRemount[index]);
+ expect(el.dataset.color).toBe('black');
+ });
+ }
+
+ it('can remount on signature change within a wrapper', () => {
+ if (__DEV__) {
+ testRemountingWithWrapper(Hello => Hello);
+ }
+ });
+
+ it('can remount on signature change within a simple memo wrapper', () => {
+ if (__DEV__) {
+ testRemountingWithWrapper(Hello => React.memo(Hello));
+ }
+ });
+
+ it('can remount on signature change within forwardRef', () => {
+ if (__DEV__) {
+ testRemountingWithWrapper(Hello => React.forwardRef(Hello));
+ }
+ });
+
+ it('can remount on signature change within forwardRef render function', () => {
+ if (__DEV__) {
+ testRemountingWithWrapper(Hello => React.forwardRef(() => ));
+ }
+ });
+
+ it('can remount on signature change within nested memo', () => {
+ if (__DEV__) {
+ testRemountingWithWrapper(Hello =>
+ React.memo(React.memo(React.memo(Hello))),
+ );
+ }
+ });
+
+ it('can remount on signature change within a memo wrapper and custom comparison', () => {
+ if (__DEV__) {
+ testRemountingWithWrapper(Hello => React.memo(Hello, () => true));
+ }
+ });
+
+ it('can remount on signature change within a class', () => {
+ if (__DEV__) {
+ testRemountingWithWrapper(Hello => {
+ const child = ;
+ return class Wrapper extends React.PureComponent {
+ render() {
+ return child;
+ }
+ };
+ });
+ }
+ });
+
+ it('can remount on signature change within a context provider', () => {
+ if (__DEV__) {
+ testRemountingWithWrapper(Hello => {
+ const Context = React.createContext();
+ const child = (
+
+
+
+ );
+ return function Wrapper() {
+ return child;
+ };
+ });
+ }
+ });
+
+ it('can remount on signature change within a context consumer', () => {
+ if (__DEV__) {
+ testRemountingWithWrapper(Hello => {
+ const Context = React.createContext();
+ const child = {() => };
+ return function Wrapper() {
+ return child;
+ };
+ });
+ }
+ });
+
+ it('can remount on signature change within a suspense node', () => {
+ if (__DEV__) {
+ testRemountingWithWrapper(Hello => {
+ // TODO: we'll probably want to test fallback trees too.
+ const child = (
+
+
+
+ );
+ return function Wrapper() {
+ return child;
+ };
+ });
+ }
+ });
+
+ it('can remount on signature change within a mode node', () => {
+ if (__DEV__) {
+ testRemountingWithWrapper(Hello => {
+ const child = (
+
+
+
+ );
+ return function Wrapper() {
+ return child;
+ };
+ });
+ }
+ });
+
+ it('can remount on signature change within a fragment node', () => {
+ if (__DEV__) {
+ testRemountingWithWrapper(Hello => {
+ const child = (
+
+
+
+ );
+ return function Wrapper() {
+ return child;
+ };
+ });
+ }
+ });
+
+ it('can remount on signature change within multiple siblings', () => {
+ if (__DEV__) {
+ testRemountingWithWrapper(Hello => {
+ const child = (
+
+
+
+
+
+
+
+ );
+ return function Wrapper() {
+ return child;
+ };
+ });
+ }
+ });
+
+ it('can remount on signature change within a profiler node', () => {
+ if (__DEV__) {
+ testRemountingWithWrapper(Hello => {
+ const child = ;
+ return function Wrapper() {
+ return (
+ {}} id="foo">
+ {child}
+
+ );
+ };
+ });
+ }
+ });
+
+ function testRemountingWithWrapper(wrap) {
+ render(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+ // When this changes, we'll expect a remount:
+ __signature__(Hello, '1');
+
+ // Use the passed wrapper.
+ // This will be different in every test.
+ return wrap(Hello);
+ });
+
+ // Bump the state before patching.
+ const el = container.firstChild;
+ expect(el.textContent).toBe('0');
+ expect(el.style.color).toBe('blue');
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(el.textContent).toBe('1');
+
+ // Perform a hot update that doesn't remount.
+ patch(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+ // The signature hasn't changed since the last time:
+ __signature__(Hello, '1');
+ return Hello;
+ });
+
+ // Assert the state was preserved but color changed.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('1');
+ expect(el.style.color).toBe('red');
+
+ // Perform a hot update that remounts.
+ patch(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ // We're changing the signature now so it will remount:
+ __register__(Hello, 'Hello');
+ __signature__(Hello, '2');
+ return Hello;
+ });
+
+ // Expect a remount.
+ expect(container.firstChild).not.toBe(el);
+ const newEl = container.firstChild;
+ expect(newEl.textContent).toBe('0');
+ expect(newEl.style.color).toBe('yellow');
+
+ // Bump state again.
+ act(() => {
+ newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(newEl.textContent).toBe('1');
+ expect(newEl.style.color).toBe('yellow');
+
+ // Verify we can patch again while preserving the signature.
+ patch(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ // Same signature as last time.
+ __register__(Hello, 'Hello');
+ __signature__(Hello, '2');
+ return Hello;
+ });
+
+ expect(container.firstChild).toBe(newEl);
+ expect(newEl.textContent).toBe('1');
+ expect(newEl.style.color).toBe('purple');
+
+ // Check removing the signature also causes a remount.
+ patch(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ // No signature this time.
+ __register__(Hello, 'Hello');
+ return Hello;
+ });
+
+ // Expect a remount.
+ expect(container.firstChild).not.toBe(newEl);
+ const finalEl = container.firstChild;
+ expect(finalEl.textContent).toBe('0');
+ expect(finalEl.style.color).toBe('orange');
+ }
+
+ it('resets hooks with dependencies on hot reload', () => {
+ if (__DEV__) {
+ let useEffectWithEmptyArrayCalls = 0;
+
+ render(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ const tranformed = React.useMemo(() => val * 2, [val]);
+ const handleClick = React.useCallback(() => setVal(v => v + 1), []);
+
+ React.useEffect(() => {
+ useEffectWithEmptyArrayCalls++;
+ }, []);
+
+ return (
+
+ {tranformed}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+ return Hello;
+ });
+
+ // Bump the state before patching.
+ const el = container.firstChild;
+ expect(el.textContent).toBe('0');
+ expect(el.style.color).toBe('blue');
+ expect(useEffectWithEmptyArrayCalls).toBe(1); // useEffect ran
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(el.textContent).toBe('2'); // val * 2
+ expect(useEffectWithEmptyArrayCalls).toBe(1); // useEffect didn't re-run
+
+ // Perform a hot update.
+ act(() => {
+ patch(() => {
+ function Hello() {
+ const [val, setVal] = React.useState(0);
+ const tranformed = React.useMemo(() => val * 10, [val]);
+ const handleClick = React.useCallback(() => setVal(v => v - 1), []);
+
+ React.useEffect(() => {
+ useEffectWithEmptyArrayCalls++;
+ }, []);
+
+ return (
+
+ {tranformed}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+ return Hello;
+ });
+ });
+
+ // Assert the state was preserved but memo was evicted.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('10'); // val * 10
+ expect(el.style.color).toBe('red');
+ expect(useEffectWithEmptyArrayCalls).toBe(2); // useEffect re-ran
+
+ // This should fire the new callback which decreases the counter.
+ act(() => {
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ });
+ expect(el.textContent).toBe('0');
+ expect(el.style.color).toBe('red');
+ expect(useEffectWithEmptyArrayCalls).toBe(2); // useEffect didn't re-run
+ }
+ });
+
+ it('can hot reload offscreen components', () => {
+ if (__DEV__) {
+ const AppV1 = prepare(() => {
+ function Hello() {
+ React.useLayoutEffect(() => {
+ Scheduler.yieldValue('Hello#layout');
+ });
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(Hello, 'Hello');
+
+ return function App({offscreen}) {
+ React.useLayoutEffect(() => {
+ Scheduler.yieldValue('App#layout');
+ });
+ return (
+
+
+
+ );
+ };
+ });
+
+ const root = ReactDOM.unstable_createRoot(container);
+ root.render();
+ expect(Scheduler).toFlushAndYieldThrough(['App#layout']);
+ const el = container.firstChild;
+ expect(el.hidden).toBe(true);
+ expect(el.firstChild).toBe(null); // Offscreen content not flushed yet.
+
+ // Perform a hot update.
+ patch(() => {
+ function Hello() {
+ React.useLayoutEffect(() => {
+ Scheduler.yieldValue('Hello#layout');
+ });
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(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.
+ expect(Scheduler).toFlushAndYieldThrough(['Hello#layout']);
+ expect(container.firstChild).toBe(el);
+ expect(el.firstChild.textContent).toBe('0');
+ expect(el.firstChild.style.color).toBe('red');
+
+ el.firstChild.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ expect(el.firstChild.textContent).toBe('0');
+ expect(el.firstChild.style.color).toBe('red');
+ expect(Scheduler).toFlushAndYieldThrough(['Hello#layout']);
+ expect(el.firstChild.textContent).toBe('1');
+ expect(el.firstChild.style.color).toBe('red');
+
+ // Hot reload while we're offscreen.
+ patch(() => {
+ function Hello() {
+ React.useLayoutEffect(() => {
+ Scheduler.yieldValue('Hello#layout');
+ });
+ const [val, setVal] = React.useState(0);
+ return (
+ setVal(val + 1)}>
+ {val}
+
+ );
+ }
+ __register__(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.
+ expect(Scheduler).toFlushAndYieldThrough(['Hello#layout']);
+ expect(container.firstChild).toBe(el);
+ expect(el.firstChild.textContent).toBe('1');
+ expect(el.firstChild.style.color).toBe('orange');
+ }
+ });
+
+ it('remounts failed error boundaries (componentDidCatch)', () => {
+ if (__DEV__) {
+ render(() => {
+ function Hello() {
+ return Hi
;
+ }
+ __register__(Hello, 'Hello');
+
+ class Boundary extends React.Component {
+ state = {error: null};
+ componentDidCatch(error) {
+ this.setState({error});
+ }
+ render() {
+ if (this.state.error) {
+ return Oops: {this.state.error.message}
;
+ }
+ return this.props.children;
+ }
+ }
+
+ function App() {
+ return (
+
+ A
+
+
+
+ B
+
+ );
+ }
+
+ return App;
+ });
+
+ expect(container.innerHTML).toBe('A
Hi
B
');
+ const firstP = container.firstChild;
+ const secondP = firstP.nextSibling.nextSibling;
+
+ // Perform a hot update that fails.
+ patch(() => {
+ function Hello() {
+ throw new Error('No');
+ }
+ __register__(Hello, 'Hello');
+ });
+
+ expect(container.innerHTML).toBe('A
Oops: No
B
');
+ expect(container.firstChild).toBe(firstP);
+ expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
+
+ // Perform a hot update that fixes the error.
+ patch(() => {
+ function Hello() {
+ return Fixed!
;
+ }
+ __register__(Hello, 'Hello');
+ });
+
+ // This should remount the error boundary (but not anything above it).
+ expect(container.innerHTML).toBe('A
Fixed!
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;
+ patch(() => {
+ function Hello() {
+ return Nice.
;
+ }
+ __register__(Hello, 'Hello');
+ });
+ expect(container.firstChild.nextSibling).toBe(helloNode);
+ expect(helloNode.textContent).toBe('Nice.');
+ }
+ });
+
+ it('remounts failed error boundaries (getDerivedStateFromError)', () => {
+ if (__DEV__) {
+ render(() => {
+ function Hello() {
+ return Hi
;
+ }
+ __register__(Hello, 'Hello');
+
+ class Boundary extends React.Component {
+ state = {error: null};
+ static getDerivedStateFromError(error) {
+ return {error};
+ }
+ render() {
+ if (this.state.error) {
+ return Oops: {this.state.error.message}
;
+ }
+ return this.props.children;
+ }
+ }
+
+ function App() {
+ return (
+
+ A
+
+
+
+ B
+
+ );
+ }
+
+ return App;
+ });
+
+ expect(container.innerHTML).toBe('A
Hi
B
');
+ const firstP = container.firstChild;
+ const secondP = firstP.nextSibling.nextSibling;
+
+ // Perform a hot update that fails.
+ patch(() => {
+ function Hello() {
+ throw new Error('No');
+ }
+ __register__(Hello, 'Hello');
+ });
+
+ expect(container.innerHTML).toBe('A
Oops: No
B
');
+ expect(container.firstChild).toBe(firstP);
+ expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
+
+ // Perform a hot update that fixes the error.
+ patch(() => {
+ function Hello() {
+ return Fixed!
;
+ }
+ __register__(Hello, 'Hello');
+ });
+
+ // This should remount the error boundary (but not anything above it).
+ expect(container.innerHTML).toBe('A
Fixed!
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;
+ patch(() => {
+ function Hello() {
+ return Nice.
;
+ }
+ __register__(Hello, 'Hello');
+ });
+ expect(container.firstChild.nextSibling).toBe(helloNode);
+ expect(helloNode.textContent).toBe('Nice.');
+ }
+ });
+
+ it('remounts error boundaries that failed asynchronously after hot update', () => {
+ if (__DEV__) {
+ render(() => {
+ function Hello() {
+ const [x] = React.useState('');
+ React.useEffect(() => {}, []);
+ x.slice(); // Doesn't throw initially.
+ return Hi
;
+ }
+ __register__(Hello, 'Hello');
+
+ class Boundary extends React.Component {
+ state = {error: null};
+ static getDerivedStateFromError(error) {
+ return {error};
+ }
+ render() {
+ if (this.state.error) {
+ return Oops: {this.state.error.message}
;
+ }
+ return this.props.children;
+ }
+ }
+
+ function App() {
+ return (
+
+ A
+
+
+
+ B
+
+ );
+ }
+
+ return App;
+ });
+
+ expect(container.innerHTML).toBe('A
Hi
B
');
+ const firstP = container.firstChild;
+ const secondP = firstP.nextSibling.nextSibling;
+
+ // Perform a hot update that fails.
+ act(() => {
+ patch(() => {
+ function Hello() {
+ const [x, setX] = React.useState('');
+ React.useEffect(() => {
+ setTimeout(() => {
+ setX(42); // This will crash next render.
+ }, 1);
+ }, []);
+ x.slice();
+ return Hi
;
+ }
+ __register__(Hello, 'Hello');
+ });
+ });
+
+ expect(container.innerHTML).toBe('A
Hi
B
');
+ // Run timeout inside effect:
+ act(() => {
+ jest.runAllTimers();
+ });
+ expect(container.innerHTML).toBe(
+ 'A
Oops: x.slice is not a function
B
',
+ );
+ expect(container.firstChild).toBe(firstP);
+ expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
+
+ // Perform a hot update that fixes the error.
+ patch(() => {
+ function Hello() {
+ const [x] = React.useState('');
+ React.useEffect(() => {}, []); // Removes the bad effect code.
+ x.slice(); // Doesn't throw initially.
+ return Fixed!
;
+ }
+ __register__(Hello, 'Hello');
+ });
+
+ // This should remount the error boundary (but not anything above it).
+ expect(container.innerHTML).toBe('A
Fixed!
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;
+ patch(() => {
+ function Hello() {
+ const [x] = React.useState('');
+ React.useEffect(() => {}, []);
+ x.slice();
+ return Nice.
;
+ }
+ __register__(Hello, 'Hello');
+ });
+ expect(container.firstChild.nextSibling).toBe(helloNode);
+ expect(helloNode.textContent).toBe('Nice.');
+ }
+ });
+});
diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js
index f51fdcfe5a..3d9def376b 100644
--- a/packages/react-reconciler/src/ReactChildFiber.js
+++ b/packages/react-reconciler/src/ReactChildFiber.js
@@ -43,6 +43,7 @@ import {
getCurrentFiberStackInDev,
getStackByFiberInDevAndProd,
} from './ReactCurrentFiber';
+import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading';
import {StrictMode} from './ReactTypeOfMode';
let didWarnAboutMaps;
@@ -378,7 +379,12 @@ function ChildReconciler(shouldTrackSideEffects) {
element: ReactElement,
expirationTime: ExpirationTime,
): Fiber {
- if (current !== null && current.elementType === element.type) {
+ if (
+ current !== null &&
+ (current.elementType === element.type ||
+ // Keep this check inline so it only runs on the false path:
+ (__DEV__ ? isCompatibleFamilyForHotReloading(current, element) : false))
+ ) {
// Move based on index
const existing = useFiber(current, element.props, expirationTime);
existing.ref = coerceRef(returnFiber, current, element);
@@ -1121,7 +1127,11 @@ function ChildReconciler(shouldTrackSideEffects) {
if (
child.tag === Fragment
? element.type === REACT_FRAGMENT_TYPE
- : child.elementType === element.type
+ : child.elementType === element.type ||
+ // Keep this check inline so it only runs on the false path:
+ (__DEV__
+ ? isCompatibleFamilyForHotReloading(child, element)
+ : false)
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(
diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js
index 1330887c14..589c482bc1 100644
--- a/packages/react-reconciler/src/ReactFiber.js
+++ b/packages/react-reconciler/src/ReactFiber.js
@@ -45,6 +45,7 @@ import {
SuspenseComponent,
FunctionComponent,
MemoComponent,
+ SimpleMemoComponent,
LazyComponent,
EventComponent,
EventTarget,
@@ -52,6 +53,10 @@ import {
import getComponentName from 'shared/getComponentName';
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
+import {
+ resolveFunctionForHotReloading,
+ resolveForwardRefForHotReloading,
+} from './ReactFiberHotReloading';
import {NoWork} from './ReactFiberExpirationTime';
import {
NoMode,
@@ -218,6 +223,7 @@ export type Fiber = {|
_debugSource?: Source | null,
_debugOwner?: Fiber | null,
_debugIsCurrentlyTiming?: boolean,
+ _debugNeedsRemount?: boolean,
// Used to verify that the order of hooks does not change between renders.
_debugHookTypes?: Array | null,
@@ -302,6 +308,7 @@ function FiberNode(
this._debugSource = null;
this._debugOwner = null;
this._debugIsCurrentlyTiming = false;
+ this._debugNeedsRemount = false;
this._debugHookTypes = null;
if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
Object.preventExtensions(this);
@@ -434,6 +441,22 @@ export function createWorkInProgress(
workInProgress.treeBaseDuration = current.treeBaseDuration;
}
+ if (__DEV__) {
+ workInProgress._debugNeedsRemount = current._debugNeedsRemount;
+ switch (workInProgress.tag) {
+ case IndeterminateComponent:
+ case FunctionComponent:
+ case SimpleMemoComponent:
+ workInProgress.type = resolveFunctionForHotReloading(current.type);
+ break;
+ case ForwardRef:
+ workInProgress.type = resolveForwardRefForHotReloading(current.type);
+ break;
+ default:
+ break;
+ }
+ }
+
return workInProgress;
}
@@ -473,6 +496,10 @@ export function createFiberFromTypeAndProps(
if (typeof type === 'function') {
if (shouldConstruct(type)) {
fiberTag = ClassComponent;
+ } else {
+ if (__DEV__) {
+ resolvedType = resolveFunctionForHotReloading(resolvedType);
+ }
}
} else if (typeof type === 'string') {
fiberTag = HostComponent;
@@ -509,6 +536,9 @@ export function createFiberFromTypeAndProps(
break getTag;
case REACT_FORWARD_REF_TYPE:
fiberTag = ForwardRef;
+ if (__DEV__) {
+ resolvedType = resolveForwardRefForHotReloading(resolvedType);
+ }
break getTag;
case REACT_MEMO_TYPE:
fiberTag = MemoComponent;
@@ -777,6 +807,7 @@ export function assignFiberPropertiesInDEV(
target._debugSource = source._debugSource;
target._debugOwner = source._debugOwner;
target._debugIsCurrentlyTiming = source._debugIsCurrentlyTiming;
+ target._debugNeedsRemount = source._debugNeedsRemount;
target._debugHookTypes = source._debugHookTypes;
return target;
}
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js
index 56208b1c43..71b931f6bb 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js
@@ -71,6 +71,7 @@ import {
getCurrentFiberStackInDev,
} from './ReactCurrentFiber';
import {startWorkTimer, cancelWorkTimer} from './ReactDebugFiberPerf';
+import {resolveFunctionForHotReloading} from './ReactFiberHotReloading';
import {
mountChildFibers,
@@ -358,18 +359,22 @@ function updateMemoComponent(
// SimpleMemoComponent codepath doesn't resolve outer props either.
Component.defaultProps === undefined
) {
+ let resolvedType = type;
+ if (__DEV__) {
+ resolvedType = resolveFunctionForHotReloading(type);
+ }
// If this is a plain function component without default props,
// and with only the default shallow comparison, we upgrade it
// to a SimpleMemoComponent to allow fast path updates.
workInProgress.tag = SimpleMemoComponent;
- workInProgress.type = type;
+ workInProgress.type = resolvedType;
if (__DEV__) {
validateFunctionComponentInDev(workInProgress, type);
}
return updateSimpleMemoComponent(
current,
workInProgress,
- type,
+ resolvedType,
nextProps,
updateExpirationTime,
renderExpirationTime,
@@ -486,7 +491,9 @@ function updateSimpleMemoComponent(
const prevProps = current.memoizedProps;
if (
shallowEqual(prevProps, nextProps) &&
- current.ref === workInProgress.ref
+ current.ref === workInProgress.ref &&
+ // Prevent bailout if the implementation changed due to hot reload:
+ (__DEV__ ? workInProgress.type === current.type : true)
) {
didReceiveUpdate = false;
if (updateExpirationTime < renderExpirationTime) {
@@ -1023,6 +1030,9 @@ function mountLazyComponent(
// Cancel and resume right after we know the tag.
cancelWorkTimer(workInProgress);
let Component = readLazyComponentType(elementType);
+ if (__DEV__) {
+ // TODO: resolve type for hot reloading.
+ }
// Store the unwrapped component in the type.
workInProgress.type = Component;
const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component));
@@ -2106,6 +2116,68 @@ function bailoutOnAlreadyFinishedWork(
}
}
+function remountFiber(
+ current: Fiber,
+ oldWorkInProgress: Fiber,
+ newWorkInProgress: Fiber,
+): Fiber | null {
+ if (__DEV__) {
+ const returnFiber = oldWorkInProgress.return;
+ if (returnFiber === null) {
+ throw new Error('Cannot swap the root fiber.');
+ }
+
+ // Disconnect from the old current.
+ // It will get deleted.
+ current.alternate = null;
+ oldWorkInProgress.alternate = null;
+
+ // Connect to the new tree.
+ newWorkInProgress.index = oldWorkInProgress.index;
+ newWorkInProgress.sibling = oldWorkInProgress.sibling;
+ newWorkInProgress.return = oldWorkInProgress.return;
+
+ // Replace the child/sibling pointers above it.
+ if (oldWorkInProgress === returnFiber.child) {
+ returnFiber.child = newWorkInProgress;
+ } else {
+ let prevSibling = returnFiber.child;
+ if (prevSibling === null) {
+ throw new Error('Expected parent to have a child.');
+ }
+ while (prevSibling.sibling !== oldWorkInProgress) {
+ prevSibling = prevSibling.sibling;
+ if (prevSibling === null) {
+ throw new Error('Expected to find the previous sibling.');
+ }
+ }
+ prevSibling.sibling = newWorkInProgress;
+ }
+
+ // Delete the old fiber and place the new one.
+ // Since the old fiber is disconnected, we have to schedule it manually.
+ const last = returnFiber.lastEffect;
+ if (last !== null) {
+ last.nextEffect = current;
+ returnFiber.lastEffect = current;
+ } else {
+ returnFiber.firstEffect = returnFiber.lastEffect = current;
+ }
+ current.nextEffect = null;
+ current.effectTag = Deletion;
+
+ newWorkInProgress.effectTag |= Placement;
+
+ // Restart work from the new fiber.
+ return newWorkInProgress;
+ } else {
+ throw new Error(
+ 'Did not expect this call in production. ' +
+ 'This is a bug in React. Please file an issue.',
+ );
+ }
+}
+
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
@@ -2113,11 +2185,34 @@ function beginWork(
): Fiber | null {
const updateExpirationTime = workInProgress.expirationTime;
+ if (__DEV__) {
+ if (workInProgress._debugNeedsRemount && current !== null) {
+ // This will restart the begin phase with a new fiber.
+ return remountFiber(
+ current,
+ workInProgress,
+ createFiberFromTypeAndProps(
+ workInProgress.type,
+ workInProgress.key,
+ workInProgress.pendingProps,
+ workInProgress._debugOwner || null,
+ workInProgress.mode,
+ workInProgress.expirationTime,
+ ),
+ );
+ }
+ }
+
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
- if (oldProps !== newProps || hasLegacyContextChanged()) {
+ if (
+ oldProps !== newProps ||
+ hasLegacyContextChanged() ||
+ // Force a re-render if the implementation changed due to hot reload:
+ (__DEV__ ? workInProgress.type !== current.type : false)
+ ) {
// If props or context changed, mark the fiber as having performed work.
// This may be unset if the props are determined to be equal later (memo).
didReceiveUpdate = true;
@@ -2256,11 +2351,10 @@ function beginWork(
switch (workInProgress.tag) {
case IndeterminateComponent: {
- const elementType = workInProgress.elementType;
return mountIndeterminateComponent(
current,
workInProgress,
- elementType,
+ workInProgress.type,
renderExpirationTime,
);
}
diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js
index 69147a0f71..f7eec92b3d 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.js
@@ -189,6 +189,11 @@ let currentHookNameInDev: ?HookType = null;
let hookTypesDev: Array | null = null;
let hookTypesUpdateIndexDev: number = -1;
+// In DEV, this tracks whether currently rendering component needs to ignore
+// the dependencies for Hooks that need them (e.g. useEffect or useMemo).
+// When true, such Hooks will always be "remounted". Only used during hot reload.
+let ignorePreviousDependencies: boolean = false;
+
function mountHookTypesDev() {
if (__DEV__) {
const hookName = ((currentHookNameInDev: any): HookType);
@@ -296,6 +301,13 @@ function areHookInputsEqual(
nextDeps: Array,
prevDeps: Array | null,
) {
+ if (__DEV__) {
+ if (ignorePreviousDependencies) {
+ // Only true when this component is being hot reloaded.
+ return false;
+ }
+ }
+
if (prevDeps === null) {
if (__DEV__) {
warning(
@@ -352,6 +364,9 @@ export function renderWithHooks(
? ((current._debugHookTypes: any): Array)
: null;
hookTypesUpdateIndexDev = -1;
+ // Used for hot reloading:
+ ignorePreviousDependencies =
+ current !== null && current.type !== workInProgress.type;
}
// The following should have already been reset
diff --git a/packages/react-reconciler/src/ReactFiberHotReloading.js b/packages/react-reconciler/src/ReactFiberHotReloading.js
new file mode 100644
index 0000000000..1804ba6fa5
--- /dev/null
+++ b/packages/react-reconciler/src/ReactFiberHotReloading.js
@@ -0,0 +1,264 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import type {ReactElement} from 'shared/ReactElementType';
+import type {Fiber} from './ReactFiber';
+import type {FiberRoot} from './ReactFiberRoot';
+
+import {
+ flushSync,
+ scheduleWork,
+ flushPassiveEffects,
+} from './ReactFiberScheduler';
+import {Sync} from './ReactFiberExpirationTime';
+import {
+ FunctionComponent,
+ ForwardRef,
+ MemoComponent,
+ SimpleMemoComponent,
+} from 'shared/ReactWorkTags';
+import {REACT_FORWARD_REF_TYPE, REACT_MEMO_TYPE} from 'shared/ReactSymbols';
+
+type Family = {|
+ currentType: any,
+ currentSignature: null | string,
+|};
+
+type HotUpdate = {|
+ familiesByType: WeakMap,
+ root: FiberRoot,
+ staleFamilies: Set,
+ updatedFamilies: Set,
+|};
+
+let familiesByType: WeakMap | null = null;
+// $FlowFixMe Flow gets confused by a WeakSet feature check below.
+let failedBoundaries: WeakSet | null = null;
+
+export function resolveFunctionForHotReloading(type: any): any {
+ if (__DEV__) {
+ if (familiesByType === null) {
+ // Hot reloading is disabled.
+ return type;
+ }
+ let family = familiesByType.get(type);
+ if (family === undefined) {
+ return type;
+ }
+ // Use the latest known implementation.
+ return family.currentType;
+ } else {
+ return type;
+ }
+}
+
+export function resolveForwardRefForHotReloading(type: any): any {
+ if (__DEV__) {
+ if (familiesByType === null) {
+ // Hot reloading is disabled.
+ return type;
+ }
+ let family = familiesByType.get(type);
+ if (family === undefined) {
+ // Check if we're dealing with a real forwardRef. Don't want to crash early.
+ if (
+ type !== null &&
+ type !== undefined &&
+ typeof type.render === 'function'
+ ) {
+ // ForwardRef is special because its resolved .type is an object,
+ // but it's possible that we only have its inner render function in the map.
+ // If that inner render function is different, we'll build a new forwardRef type.
+ const currentRender = resolveFunctionForHotReloading(type.render);
+ if (type.render !== currentRender) {
+ const syntheticType = {
+ $$typeof: REACT_FORWARD_REF_TYPE,
+ render: currentRender,
+ };
+ if (type.displayName !== undefined) {
+ (syntheticType: any).displayName = type.displayName;
+ }
+ return syntheticType;
+ }
+ }
+ return type;
+ }
+ // Use the latest known implementation.
+ return family.currentType;
+ } else {
+ return type;
+ }
+}
+
+export function isCompatibleFamilyForHotReloading(
+ fiber: Fiber,
+ element: ReactElement,
+): boolean {
+ if (__DEV__) {
+ if (familiesByType === null) {
+ // Hot reloading is disabled.
+ return false;
+ }
+
+ const prevType = fiber.elementType;
+ const nextType = element.type;
+ // If we got here, we know types aren't === equal.
+ let needsCompareFamilies = false;
+ switch (fiber.tag) {
+ case FunctionComponent: {
+ if (typeof nextType === 'function') {
+ needsCompareFamilies = true;
+ }
+ break;
+ }
+ case ForwardRef: {
+ if (
+ typeof nextType === 'object' &&
+ nextType !== null &&
+ nextType.$$typeof === REACT_FORWARD_REF_TYPE
+ ) {
+ needsCompareFamilies = true;
+ }
+ break;
+ }
+ case MemoComponent:
+ case SimpleMemoComponent: {
+ if (
+ typeof nextType === 'object' &&
+ nextType !== null &&
+ nextType.$$typeof === REACT_MEMO_TYPE
+ ) {
+ // TODO: if it was but can no longer be simple,
+ // we shouldn't set this.
+ needsCompareFamilies = true;
+ }
+ break;
+ }
+ // TODO: maybe support lazy?
+ default:
+ return false;
+ }
+
+ // Check if both types have a family and it's the same one.
+ if (needsCompareFamilies) {
+ // Note: memo() and forwardRef() we'll compare outer rather than inner type.
+ // This means both of them need to be registered to preserve state.
+ // If we unwrapped and compared the inner types for wrappers instead,
+ // then we would risk falsely saying two separate memo(Foo)
+ // calls are equivalent because they wrap the same Foo function.
+ const prevFamily = familiesByType.get(prevType);
+ if (
+ prevFamily !== undefined &&
+ prevFamily === familiesByType.get(nextType)
+ ) {
+ return true;
+ }
+ }
+ return false;
+ } else {
+ return false;
+ }
+}
+
+export function markFailedErrorBoundaryForHotReloading(fiber: Fiber) {
+ if (__DEV__) {
+ if (familiesByType === null) {
+ // Not hot reloading.
+ return;
+ }
+ if (typeof WeakSet !== 'function') {
+ return;
+ }
+ if (failedBoundaries === null) {
+ failedBoundaries = new WeakSet();
+ }
+ failedBoundaries.add(fiber);
+ }
+}
+
+export function scheduleHotUpdate(hotUpdate: HotUpdate): void {
+ if (__DEV__) {
+ // TODO: warn if its identity changes over time?
+ familiesByType = hotUpdate.familiesByType;
+
+ const {root, staleFamilies, updatedFamilies} = hotUpdate;
+ flushPassiveEffects();
+ flushSync(() => {
+ scheduleFibersWithFamiliesRecursively(
+ root.current,
+ updatedFamilies,
+ staleFamilies,
+ );
+ });
+ }
+}
+
+function scheduleFibersWithFamiliesRecursively(
+ fiber: Fiber,
+ updatedFamilies: Set,
+ staleFamilies: Set,
+) {
+ if (__DEV__) {
+ const {alternate, child, sibling, tag, type} = fiber;
+
+ let candidateType = null;
+ switch (tag) {
+ case FunctionComponent:
+ case SimpleMemoComponent:
+ candidateType = type;
+ break;
+ case ForwardRef:
+ candidateType = type.render;
+ break;
+ default:
+ break;
+ }
+
+ if (familiesByType === null) {
+ throw new Error('Expected familiesByType to be set during hot reload.');
+ }
+
+ if (candidateType !== null) {
+ const family = familiesByType.get(candidateType);
+ if (family !== undefined) {
+ if (staleFamilies.has(family)) {
+ fiber._debugNeedsRemount = true;
+ scheduleWork(fiber, Sync);
+ } else if (updatedFamilies.has(family)) {
+ scheduleWork(fiber, Sync);
+ }
+ }
+ }
+
+ if (failedBoundaries !== null) {
+ if (
+ failedBoundaries.has(fiber) ||
+ (alternate !== null && failedBoundaries.has(alternate))
+ ) {
+ fiber._debugNeedsRemount = true;
+ scheduleWork(fiber, Sync);
+ }
+ }
+
+ if (child !== null) {
+ scheduleFibersWithFamiliesRecursively(
+ child,
+ updatedFamilies,
+ staleFamilies,
+ );
+ }
+ if (sibling !== null) {
+ scheduleFibersWithFamiliesRecursively(
+ sibling,
+ updatedFamilies,
+ staleFamilies,
+ );
+ }
+ }
+}
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js
index ae3d449339..84291f4b7a 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.js
@@ -67,6 +67,7 @@ import {StrictMode} from './ReactTypeOfMode';
import {Sync} from './ReactFiberExpirationTime';
import {revertPassiveEffectsChange} from 'shared/ReactFeatureFlags';
import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig';
+import {scheduleHotUpdate} from './ReactFiberHotReloading';
type OpaqueRoot = FiberRoot;
@@ -460,6 +461,7 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
return injectInternals({
...devToolsConfig,
+ scheduleHotUpdate: __DEV__ ? scheduleHotUpdate : null,
overrideHookState,
overrideProps,
setSuspenseHandler,
diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js
index 4db7a3a76a..f15815ffe3 100644
--- a/packages/react-reconciler/src/ReactFiberUnwindWork.js
+++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js
@@ -56,6 +56,7 @@ import {
import {logError} from './ReactFiberCommitWork';
import {getStackByFiberInDevAndProd} from './ReactCurrentFiber';
import {popHostContainer, popHostContext} from './ReactFiberHostContext';
+import {markFailedErrorBoundaryForHotReloading} from './ReactFiberHotReloading';
import {
suspenseStackCursor,
InvisibleParentSuspenseContext,
@@ -122,6 +123,9 @@ function createClassErrorUpdate(
const inst = fiber.stateNode;
if (inst !== null && typeof inst.componentDidCatch === 'function') {
update.callback = function callback() {
+ if (__DEV__) {
+ markFailedErrorBoundaryForHotReloading(fiber);
+ }
if (typeof getDerivedStateFromError !== 'function') {
// To preserve the preexisting retry behavior of error boundaries,
// we keep track of which ones already failed during this batch.
@@ -150,6 +154,10 @@ function createClassErrorUpdate(
}
}
};
+ } else if (__DEV__) {
+ update.callback = () => {
+ markFailedErrorBoundaryForHotReloading(fiber);
+ };
}
return update;
}
diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorReplay-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorReplay-test.internal.js
index 6e6215efcb..9a497f074d 100644
--- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorReplay-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorReplay-test.internal.js
@@ -10,21 +10,22 @@
'use strict';
describe('ReactIncrementalErrorReplay-test', () => {
+ const React = require('react');
+ const ReactTestRenderer = require('react-test-renderer');
+
it('copies all keys when stashing potentially failing work', () => {
// Note: this test is fragile and relies on internals.
// We almost always try to avoid such tests, but here the cost of
// the list getting out of sync (and causing subtle bugs in rare cases)
// is higher than the cost of maintaining the test.
const {
- // Any Fiber factory function will do.
- createHostRootFiber,
// This is the method we're going to test.
// If this is no longer used, you can delete this test file.
assignFiberPropertiesInDEV,
} = require('../ReactFiber');
// Get a real fiber.
- const realFiber = createHostRootFiber(false);
+ const realFiber = ReactTestRenderer.create().root._currentFiber();
const stash = assignFiberPropertiesInDEV(null, realFiber);
// Verify we get all the same fields.