Skip to content

Commit 26352ca

Browse files
Fix nested batch scheduling (#120)
1 parent 1b4fe6d commit 26352ca

File tree

6 files changed

+117
-19
lines changed

6 files changed

+117
-19
lines changed

babel.config.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@ module.exports = {
55
[
66
'@babel/preset-env',
77
{
8-
targets: { edge: '16' },
8+
targets: [
9+
'last 2 chrome versions',
10+
'last 2 firefox versions',
11+
'last 2 safari versions',
12+
'last 2 and_chr versions',
13+
'last 2 ios_saf versions',
14+
'edge >= 18',
15+
],
916
modules: false,
10-
exclude: ['transform-typeof-symbol'],
1117
loose: true,
1218
},
1319
],

src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ export { createSubscriber } from './components/subscriber';
55
export { createHook } from './components/hook';
66
export { default as defaults } from './defaults';
77
export { createStore, defaultRegistry } from './store';
8-
export { unstable_batchedUpdates as batch } from './utils/batched-updates';
8+
export { batch } from './utils/batched-updates';
99
export { createSelector } from './utils/create-selector';
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/* eslint-env jest */
2+
3+
import React, { useState } from 'react';
4+
import { mount } from 'enzyme';
5+
6+
import { createHook } from '../../components/hook';
7+
import defaults from '../../defaults';
8+
import { createStore, defaultRegistry } from '../../store';
9+
import supports from '../../utils/supported-features';
10+
import { batch } from '../batched-updates';
11+
12+
const Store = createStore({
13+
initialState: { count: 0 },
14+
actions: {
15+
increment: () => ({ getState, setState }) => {
16+
setState({ count: getState().count + 1 });
17+
},
18+
},
19+
});
20+
21+
const useHook = createHook(Store);
22+
23+
describe('batch', () => {
24+
const TestComponent = ({ children }) => {
25+
const [{ count }, actions] = useHook();
26+
const [localCount, setLocalCount] = useState(0);
27+
const update = () =>
28+
batch(() => {
29+
actions.increment();
30+
setLocalCount(localCount + 1);
31+
});
32+
33+
return children(update, count, localCount);
34+
};
35+
36+
beforeEach(() => {
37+
defaultRegistry.stores.clear();
38+
});
39+
40+
it('should batch updates with scheduling disabled', () => {
41+
const child = jest.fn().mockReturnValue(null);
42+
mount(<TestComponent>{child}</TestComponent>);
43+
const update = child.mock.calls[0][0];
44+
update();
45+
46+
expect(child.mock.calls).toHaveLength(2);
47+
expect(child.mock.calls[1]).toEqual([expect.any(Function), 1, 1]);
48+
});
49+
50+
it('should batch updates with scheduling enabled', async () => {
51+
const supportsMock = jest
52+
.spyOn(supports, 'scheduling')
53+
.mockReturnValue(true);
54+
defaults.batchUpdates = true;
55+
56+
const child = jest.fn().mockReturnValue(null);
57+
mount(<TestComponent>{child}</TestComponent>);
58+
const update = child.mock.calls[0][0];
59+
update();
60+
61+
// scheduler uses timeouts on non-browser envs
62+
await new Promise((r) => setTimeout(r, 10));
63+
64+
expect(child.mock.calls).toHaveLength(2);
65+
expect(child.mock.calls[1]).toEqual([expect.any(Function), 1, 1]);
66+
67+
supportsMock.mockRestore();
68+
defaults.batchUpdates = false;
69+
});
70+
});

src/utils/batched-updates.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,33 @@
11
/* eslint-disable import/no-unresolved */
2-
export { unstable_batchedUpdates } from 'react-dom';
2+
import { unstable_batchedUpdates } from 'react-dom';
3+
import {
4+
unstable_scheduleCallback as scheduleCallback,
5+
unstable_UserBlockingPriority as UserBlockingPriority,
6+
} from 'scheduler';
7+
8+
import defaults from '../defaults';
9+
import supports from './supported-features';
10+
11+
let isInsideBatchedSchedule = false;
12+
13+
export function batch(fn) {
14+
// if we are in node/tests or feature disabled or nested schedule
15+
if (
16+
!defaults.batchUpdates ||
17+
!supports.scheduling() ||
18+
isInsideBatchedSchedule
19+
) {
20+
return unstable_batchedUpdates(fn);
21+
}
22+
23+
isInsideBatchedSchedule = true;
24+
// Use UserBlockingPriority as it has max 250ms timeout
25+
// https://github.com/facebook/react/blob/master/packages/scheduler/src/forks/SchedulerNoDOM.js#L47
26+
return scheduleCallback(
27+
UserBlockingPriority,
28+
function scheduleBatchedUpdates() {
29+
unstable_batchedUpdates(fn);
30+
isInsideBatchedSchedule = false;
31+
}
32+
);
33+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
/* eslint-disable import/no-unresolved */
2-
export { unstable_batchedUpdates } from 'react-native';
2+
export { unstable_batchedUpdates as batch } from 'react-native';

src/utils/schedule.js

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
import {
2-
unstable_scheduleCallback as scheduleCallback,
3-
unstable_UserBlockingPriority as UserBlockingPriority,
4-
} from 'scheduler';
51
import defaults from '../defaults';
6-
import { unstable_batchedUpdates as batch } from './batched-updates';
2+
import { batch } from './batched-updates';
73
import supports from './supported-features';
84

95
const QUEUE = [];
@@ -20,14 +16,9 @@ export default function schedule(fn) {
2016

2117
// if something already started schedule, skip
2218
if (scheduled) return;
23-
24-
// Use UserBlockingPriority as it has max 250ms timeout
25-
// https://github.com/facebook/react/blob/master/packages/scheduler/src/forks/SchedulerNoDOM.js#L47
26-
scheduled = scheduleCallback(UserBlockingPriority, function runNotifyQueue() {
27-
batch(() => {
28-
let queueFn;
29-
while ((queueFn = QUEUE.shift())) queueFn();
30-
scheduled = null;
31-
});
19+
scheduled = batch(() => {
20+
let queueFn;
21+
while ((queueFn = QUEUE.shift())) queueFn();
22+
scheduled = null;
3223
});
3324
}

0 commit comments

Comments
 (0)