diff --git a/packages/react-dom/src/__tests__/ReactUpdates-test.js b/packages/react-dom/src/__tests__/ReactUpdates-test.js index 591ffa2d31..0b6a4b2792 100644 --- a/packages/react-dom/src/__tests__/ReactUpdates-test.js +++ b/packages/react-dom/src/__tests__/ReactUpdates-test.js @@ -1290,6 +1290,78 @@ describe('ReactUpdates', () => { expect(ops).toEqual(['Foo', 'Bar', 'Baz']); }); + it('delays sync updates inside hidden subtrees in Concurrent Mode', () => { + const container = document.createElement('div'); + + function Baz() { + Scheduler.yieldValue('Baz'); + return

baz

; + } + + let setCounter; + function Bar() { + const [counter, _setCounter] = React.useState(0); + setCounter = _setCounter; + Scheduler.yieldValue('Bar'); + return

bar {counter}

; + } + + function Foo() { + Scheduler.yieldValue('Foo'); + React.useEffect(() => { + Scheduler.yieldValue('Foo#effect'); + }); + return ( +
+ + +
+ ); + } + + const root = ReactDOM.unstable_createRoot(container); + root.render(); + if (__DEV__) { + expect(Scheduler).toFlushAndYieldThrough([ + 'Foo', + 'Foo', + 'Baz', + 'Foo#effect', + ]); + } else { + expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Baz', 'Foo#effect']); + } + + const hiddenDiv = container.firstChild.firstChild; + expect(hiddenDiv.hidden).toBe(true); + expect(hiddenDiv.innerHTML).toBe(''); + + // Run offscreen update + if (__DEV__) { + expect(Scheduler).toFlushAndYield(['Bar', 'Bar']); + } else { + expect(Scheduler).toFlushAndYield(['Bar']); + } + expect(hiddenDiv.hidden).toBe(true); + expect(hiddenDiv.innerHTML).toBe('

bar 0

'); + + ReactDOM.flushSync(() => { + setCounter(1); + }); + // Should not flush yet + expect(hiddenDiv.innerHTML).toBe('

bar 0

'); + + // Run offscreen update + if (__DEV__) { + expect(Scheduler).toFlushAndYield(['Bar', 'Bar']); + } else { + expect(Scheduler).toFlushAndYield(['Bar']); + } + expect(hiddenDiv.innerHTML).toBe('

bar 1

'); + }); + it('can render ridiculously large number of roots without triggering infinite update loop error', () => { class Foo extends React.Component { componentDidMount() { diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 97a99f005e..56208b1c43 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -973,8 +973,8 @@ function updateHostComponent(current, workInProgress, renderExpirationTime) { // Check the host config to see if the children are offscreen/hidden. if ( - renderExpirationTime !== Never && workInProgress.mode & ConcurrentMode && + renderExpirationTime !== Never && shouldDeprioritizeSubtree(type, nextProps) ) { // Schedule this fiber to re-render at offscreen priority. Then bailout. @@ -2133,6 +2133,15 @@ function beginWork( break; case HostComponent: pushHostContext(workInProgress); + if ( + workInProgress.mode & ConcurrentMode && + renderExpirationTime !== Never && + shouldDeprioritizeSubtree(workInProgress.type, newProps) + ) { + // Schedule this fiber to re-render at offscreen priority. Then bailout. + workInProgress.expirationTime = workInProgress.childExpirationTime = Never; + return null; + } break; case ClassComponent: { const Component = workInProgress.type; diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.internal.js index 5d10f94eec..268233f68c 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.internal.js @@ -966,7 +966,7 @@ describe('ReactIncrementalSideEffects', () => { // However, once we render fully, we will have enough time to finish it all // at once. - expect(Scheduler).toFlushAndYield(['Bar', 'Bar', 'Bar']); + expect(Scheduler).toFlushAndYield(['Bar', 'Bar']); expect(ReactNoop.getChildrenAsJSX()).toEqual(