Skip to content

Commit f109716

Browse files
committed
Batch updates in flushSync
Previously, flushSync temporarily configured options.debounceRendering to execute every scheduled render immediately. This made updates synchronous, but it also caused each state setter to render independently instead of batching all updates performed inside the callback into one synchronous commit. Capture the scheduled render callback while the user callback runs, then flush it once before returning. The previous debounce function is restored in a finally block. Mirrors #5173 (v10.x).
1 parent 6254ee1 commit f109716

2 files changed

Lines changed: 73 additions & 4 deletions

File tree

compat/src/index.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,21 @@ function findDOMNode(component) {
132132
*/
133133
const flushSync = (callback, arg) => {
134134
const prevDebounce = options.debounceRendering;
135-
options.debounceRendering = cb => cb();
136-
const res = callback(arg);
137-
options.debounceRendering = prevDebounce;
138-
return res;
135+
136+
// Capture the scheduled render callback so that all updates performed
137+
// inside the user callback are batched into a single synchronous commit
138+
// instead of every state setter rendering independently.
139+
let flush;
140+
options.debounceRendering = cb => {
141+
flush = cb;
142+
};
143+
try {
144+
const res = callback(arg);
145+
if (flush) flush();
146+
return res;
147+
} finally {
148+
options.debounceRendering = prevDebounce;
149+
}
139150
};
140151

141152
/**
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { createElement, render } from 'preact';
2+
import { useState } from 'preact/hooks';
3+
import { unstable_batchedUpdates, flushSync } from 'preact/compat';
4+
import { vi } from 'vitest';
5+
import { setupScratch, teardown } from '../../../test/_util/helpers';
6+
7+
describe('unstable_batchedUpdates', () => {
8+
it('should execute & return cb', () => {
9+
expect(unstable_batchedUpdates(() => false)).to.equal(false);
10+
expect(unstable_batchedUpdates(arg => arg, true)).to.equal(true);
11+
});
12+
});
13+
14+
describe('flushSync', () => {
15+
/** @type {HTMLDivElement} */
16+
let scratch;
17+
18+
beforeEach(() => {
19+
scratch = setupScratch();
20+
});
21+
22+
afterEach(() => {
23+
teardown(scratch);
24+
});
25+
26+
it('should invoke the given callback', () => {
27+
const returnValue = {};
28+
const spy = vi.fn(() => returnValue);
29+
const result = flushSync(spy);
30+
31+
expect(spy).toHaveBeenCalledOnce();
32+
expect(result).to.equal(returnValue);
33+
});
34+
35+
it('should batch updates and flush them synchronously', () => {
36+
let setA;
37+
let setB;
38+
const renders = vi.fn();
39+
40+
function App() {
41+
const [a, updateA] = useState(0);
42+
const [b, updateB] = useState(0);
43+
setA = updateA;
44+
setB = updateB;
45+
renders();
46+
return <p>{a + b}</p>;
47+
}
48+
49+
render(<App />, scratch);
50+
flushSync(() => {
51+
setA(1);
52+
setB(1);
53+
});
54+
55+
expect(scratch.textContent).to.equal('2');
56+
expect(renders).toHaveBeenCalledTimes(2);
57+
});
58+
});

0 commit comments

Comments
 (0)