diff --git a/packages/react-dom/src/__tests__/ReactDOMHooks-test.js b/packages/react-dom/src/__tests__/ReactDOMHooks-test.js index d317e407e2e1b..fe6caa82198da 100644 --- a/packages/react-dom/src/__tests__/ReactDOMHooks-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMHooks-test.js @@ -12,7 +12,7 @@ let React; let ReactDOM; -describe('ReactDOMSuspensePlaceholder', () => { +describe('ReactDOMHooks', () => { let container; beforeEach(() => { @@ -29,6 +29,83 @@ describe('ReactDOMSuspensePlaceholder', () => { document.body.removeChild(container); }); + it('can ReactDOM.render() from useEffect', () => { + let container2 = document.createElement('div'); + let container3 = document.createElement('div'); + + function Example1({n}) { + React.useEffect(() => { + ReactDOM.render(, container2); + }); + return 1 * n; + } + + function Example2({n}) { + React.useEffect(() => { + ReactDOM.render(, container3); + }); + return 2 * n; + } + + function Example3({n}) { + return 3 * n; + } + + ReactDOM.render(, container); + expect(container.textContent).toBe('1'); + expect(container2.textContent).toBe(''); + expect(container3.textContent).toBe(''); + jest.runAllTimers(); + expect(container.textContent).toBe('1'); + expect(container2.textContent).toBe('2'); + expect(container3.textContent).toBe('3'); + + ReactDOM.render(, container); + expect(container.textContent).toBe('2'); + expect(container2.textContent).toBe('2'); // Not flushed yet + expect(container3.textContent).toBe('3'); // Not flushed yet + jest.runAllTimers(); + expect(container.textContent).toBe('2'); + expect(container2.textContent).toBe('4'); + expect(container3.textContent).toBe('6'); + }); + + it('can batch synchronous work inside effects with other work', () => { + let otherContainer = document.createElement('div'); + + let calledA = false; + function A() { + calledA = true; + return 'A'; + } + + let calledB = false; + function B() { + calledB = true; + return 'B'; + } + + let _set; + function Foo() { + _set = React.useState(0)[1]; + React.useEffect(() => { + ReactDOM.render(, otherContainer); + }); + return null; + } + + ReactDOM.render(, container); + ReactDOM.unstable_batchedUpdates(() => { + _set(0); // Forces the effect to be flushed + expect(otherContainer.textContent).toBe(''); + ReactDOM.render(, otherContainer); + expect(otherContainer.textContent).toBe(''); + }); + expect(otherContainer.textContent).toBe('B'); + expect(calledA).toBe(false); // It was in a batch + expect(calledB).toBe(true); + }); + it('should not bail out when an update is scheduled from within an event handler', () => { const {createRef, useCallback, useState} = React; diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index de4fa2b0a053e..2e05987a3d3d4 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -572,6 +572,10 @@ function commitPassiveEffects(root: FiberRoot, firstEffect: Fiber): void { if (rootExpirationTime !== NoWork) { requestWork(root, rootExpirationTime); } + // Flush any sync work that was scheduled by effects + if (!isBatchingUpdates && !isRendering) { + performSyncWork(); + } } function isAlreadyFailedLegacyErrorBoundary(instance: mixed): boolean {