UNPKG

5.21 kBPlain TextView Raw
1import type { StoreEnhancer } from 'redux'
2
3export const SHOULD_AUTOBATCH = 'RTK_autoBatch'
4
5export const prepareAutoBatched =
6 <T>() =>
7 (payload: T): { payload: T; meta: unknown } => ({
8 payload,
9 meta: { [SHOULD_AUTOBATCH]: true },
10 })
11
12const createQueueWithTimer = (timeout: number) => {
13 return (notify: () => void) => {
14 setTimeout(notify, timeout)
15 }
16}
17
18// requestAnimationFrame won't exist in SSR environments.
19// Fall back to a vague approximation just to keep from erroring.
20const rAF =
21 typeof window !== 'undefined' && window.requestAnimationFrame
22 ? window.requestAnimationFrame
23 : createQueueWithTimer(10)
24
25export type AutoBatchOptions =
26 | { type: 'tick' }
27 | { type: 'timer'; timeout: number }
28 | { type: 'raf' }
29 | { type: 'callback'; queueNotification: (notify: () => void) => void }
30
31/**
32 * A Redux store enhancer that watches for "low-priority" actions, and delays
33 * notifying subscribers until either the queued callback executes or the
34 * next "standard-priority" action is dispatched.
35 *
36 * This allows dispatching multiple "low-priority" actions in a row with only
37 * a single subscriber notification to the UI after the sequence of actions
38 * is finished, thus improving UI re-render performance.
39 *
40 * Watches for actions with the `action.meta[SHOULD_AUTOBATCH]` attribute.
41 * This can be added to `action.meta` manually, or by using the
42 * `prepareAutoBatched` helper.
43 *
44 * By default, it will queue a notification for the end of the event loop tick.
45 * However, you can pass several other options to configure the behavior:
46 * - `{type: 'tick'}`: queues using `queueMicrotask`
47 * - `{type: 'timer', timeout: number}`: queues using `setTimeout`
48 * - `{type: 'raf'}`: queues using `requestAnimationFrame` (default)
49 * - `{type: 'callback', queueNotification: (notify: () => void) => void}`: lets you provide your own callback
50 *
51 *
52 */
53export const autoBatchEnhancer =
54 (options: AutoBatchOptions = { type: 'raf' }): StoreEnhancer =>
55 (next) =>
56 (...args) => {
57 const store = next(...args)
58
59 let notifying = true
60 let shouldNotifyAtEndOfTick = false
61 let notificationQueued = false
62
63 const listeners = new Set<() => void>()
64
65 const queueCallback =
66 options.type === 'tick'
67 ? queueMicrotask
68 : options.type === 'raf'
69 ? rAF
70 : options.type === 'callback'
71 ? options.queueNotification
72 : createQueueWithTimer(options.timeout)
73
74 const notifyListeners = () => {
75 // We're running at the end of the event loop tick.
76 // Run the real listener callbacks to actually update the UI.
77 notificationQueued = false
78 if (shouldNotifyAtEndOfTick) {
79 shouldNotifyAtEndOfTick = false
80 listeners.forEach((l) => l())
81 }
82 }
83
84 return Object.assign({}, store, {
85 // Override the base `store.subscribe` method to keep original listeners
86 // from running if we're delaying notifications
87 subscribe(listener: () => void) {
88 // Each wrapped listener will only call the real listener if
89 // the `notifying` flag is currently active when it's called.
90 // This lets the base store work as normal, while the actual UI
91 // update becomes controlled by this enhancer.
92 const wrappedListener: typeof listener = () => notifying && listener()
93 const unsubscribe = store.subscribe(wrappedListener)
94 listeners.add(listener)
95 return () => {
96 unsubscribe()
97 listeners.delete(listener)
98 }
99 },
100 // Override the base `store.dispatch` method so that we can check actions
101 // for the `shouldAutoBatch` flag and determine if batching is active
102 dispatch(action: any) {
103 try {
104 // If the action does _not_ have the `shouldAutoBatch` flag,
105 // we resume/continue normal notify-after-each-dispatch behavior
106 notifying = !action?.meta?.[SHOULD_AUTOBATCH]
107 // If a `notifyListeners` microtask was queued, you can't cancel it.
108 // Instead, we set a flag so that it's a no-op when it does run
109 shouldNotifyAtEndOfTick = !notifying
110 if (shouldNotifyAtEndOfTick) {
111 // We've seen at least 1 action with `SHOULD_AUTOBATCH`. Try to queue
112 // a microtask to notify listeners at the end of the event loop tick.
113 // Make sure we only enqueue this _once_ per tick.
114 if (!notificationQueued) {
115 notificationQueued = true
116 queueCallback(notifyListeners)
117 }
118 }
119 // Go ahead and process the action as usual, including reducers.
120 // If normal notification behavior is enabled, the store will notify
121 // all of its own listeners, and the wrapper callbacks above will
122 // see `notifying` is true and pass on to the real listener callbacks.
123 // If we're "batching" behavior, then the wrapped callbacks will
124 // bail out, causing the base store notification behavior to be no-ops.
125 return store.dispatch(action)
126 } finally {
127 // Assume we're back to normal behavior after each action
128 notifying = true
129 }
130 },
131 })
132 }