From 3653f10f836c3b6a12cc1b70f36fcf9eb32597e2 Mon Sep 17 00:00:00 2001
From: Dan setVal(val + 1)}>
+ {val}
+ setVal(val + 1)}>
+ {val}
+ setVal(val + 1)}>
+ {val}
+ setVal(val + 1)}>
+ {val}
+ setVal(val + 1)}>
+ {val}
+
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'); + + // 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 a stale type. + 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 lazy 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 patch lazy before resolution', async () => { + if (__DEV__) { + let promise; + render(() => { + function Hello() { + const [val, setVal] = React.useState(0); + return ( +setVal(val + 1)}> + {val} +
+ ); + } + __register__(Hello, 'Hello'); + + const Outer = React.lazy(() => { + promise = new Promise(resolve => { + setTimeout(() => resolve({default: Hello}), 100); + }); + return promise; + }); + __register__(Outer, 'Outer'); + + function App() { + return ( +setVal(val + 1)}> + {val} +
+ ); + } + __register__(Hello, 'Hello'); + }); + + jest.runAllTimers(); + await promise; + Scheduler.flushAll(); + + // Expect different color on initial mount. + const el = container.firstChild; + expect(el.textContent).toBe('0'); + expect(el.style.color).toBe('red'); + + // Bump state. + 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. + patch(() => { + function Hello() { + const [val, setVal] = React.useState(0); + return ( +setVal(val + 1)}> + {val} +
+ ); + } + __register__(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 promise; + render(() => { + function renderHello() { + const [val, setVal] = React.useState(0); + return ( +setVal(val + 1)}> + {val} +
+ ); + } + const Hello = React.forwardRef(renderHello); + __register__(Hello, 'Hello'); + + const Outer = React.lazy(() => { + promise = new Promise(resolve => { + setTimeout(() => resolve({default: Hello}), 100); + }); + return promise; + }); + __register__(Outer, 'Outer'); + + function App() { + return ( +setVal(val + 1)}> + {val} +
+ ); + } + const Hello = React.forwardRef(renderHello); + __register__(Hello, 'Hello'); + }); + + jest.runAllTimers(); + await promise; + Scheduler.flushAll(); + + // Expect different color on initial mount. + const el = container.firstChild; + expect(el.textContent).toBe('0'); + expect(el.style.color).toBe('red'); + + // Bump state. + 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. + patch(() => { + function renderHello() { + const [val, setVal] = React.useState(0); + return ( +setVal(val + 1)}> + {val} +
+ ); + } + const Hello = React.forwardRef(renderHello); + __register__(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 promise; + render(() => { + function renderHello() { + const [val, setVal] = React.useState(0); + return ( +setVal(val + 1)}> + {val} +
+ ); + } + const Hello = React.memo(renderHello); + __register__(Hello, 'Hello'); + + const Outer = React.lazy(() => { + promise = new Promise(resolve => { + setTimeout(() => resolve({default: Hello}), 100); + }); + return promise; + }); + __register__(Outer, 'Outer'); + + function App() { + return ( +setVal(val + 1)}> + {val} +
+ ); + } + const Hello = React.memo(renderHello); + __register__(Hello, 'Hello'); + }); + + jest.runAllTimers(); + await promise; + Scheduler.flushAll(); + + // Expect different color on initial mount. + const el = container.firstChild; + expect(el.textContent).toBe('0'); + expect(el.style.color).toBe('red'); + + // Bump state. + 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. + patch(() => { + function renderHello() { + const [val, setVal] = React.useState(0); + return ( +setVal(val + 1)}> + {val} +
+ ); + } + const Hello = React.memo(renderHello); + __register__(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 promise; + render(() => { + function renderHello() { + const [val, setVal] = React.useState(0); + return ( +setVal(val + 1)}> + {val} +
+ ); + } + const Hello = React.memo(React.forwardRef(renderHello)); + __register__(Hello, 'Hello'); + + const Outer = React.lazy(() => { + promise = new Promise(resolve => { + setTimeout(() => resolve({default: Hello}), 100); + }); + return promise; + }); + __register__(Outer, 'Outer'); + + function App() { + return ( +setVal(val + 1)}> + {val} +
+ ); + } + const Hello = React.memo(React.forwardRef(renderHello)); + __register__(Hello, 'Hello'); + }); + + jest.runAllTimers(); + await promise; + Scheduler.flushAll(); + + // Expect different color on initial mount. + const el = container.firstChild; + expect(el.textContent).toBe('0'); + expect(el.style.color).toBe('red'); + + // Bump state. + 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. + patch(() => { + function renderHello() { + const [val, setVal] = React.useState(0); + return ( +setVal(val + 1)}> + {val} +
+ ); + } + const Hello = React.memo(React.forwardRef(renderHello)); + __register__(Hello, 'Hello'); + }); + expect(container.firstChild).toBe(el); + expect(el.textContent).toBe('1'); + expect(el.style.color).toBe('orange'); + } + }); + + it('can patch both trees while suspense is displaying the fallback', async () => { + if (__DEV__) { + const AppV1 = render( + () => { + function Hello({children}) { + const [val, setVal] = React.useState(0); + return ( +setVal(val + 1)}> + {children} {val} +
+ ); + } + __register__(Hello, 'Hello'); + + function Never() { + throw new Promise(resolve => {}); + } + + function App({shouldSuspend}) { + return ( +setVal(val + 1)}> + {children} {val} +
+ ); + } + __register__(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. + 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. + 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. + patch(() => { + function Hello({children}) { + const [val, setVal] = React.useState(0); + return ( +setVal(val + 1)}> + {children} {val} +
+ ); + } + __register__(Hello, 'Hello'); + }); + + // Colors inside both trees should change: + 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('red'); + 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: + 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. + patch(() => { + function Hello({children}) { + const [val, setVal] = React.useState(0); + return ( +setVal(val + 1)}> + {children} {val} +
+ ); + } + __register__(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', () => { if (__DEV__) { let appRenders = 0; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 71b931f6bba6..761c128de63f 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -1030,9 +1030,6 @@ 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)); @@ -1043,6 +1040,9 @@ function mountLazyComponent( case FunctionComponent: { if (__DEV__) { validateFunctionComponentInDev(workInProgress, Component); + workInProgress.type = Component = resolveFunctionForHotReloading( + Component, + ); } child = updateFunctionComponent( null, @@ -1064,6 +1064,11 @@ function mountLazyComponent( break; } case ForwardRef: { + if (__DEV__) { + workInProgress.type = Component = resolveFunctionForHotReloading( + Component, + ); + } child = updateForwardRef( null, workInProgress, From 2fb5d0ed002d39b0a6b36f9f67601220c814b1bc Mon Sep 17 00:00:00 2001 From: Dan Abramov