Skip to content

Commit 24aade4

Browse files
committed
Fix documentation around nested dispatches and add test to verify it
1 parent cdb296c commit 24aade4

File tree

3 files changed

+59
-5
lines changed

3 files changed

+59
-5
lines changed

docs/api/Store.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ Adds a change listener. It will be called any time an action is dispatched, and
8484

8585
You may call [`dispatch()`](#dispatch) from a change listener, with the following caveats:
8686

87-
1. Both subscription and unsubscription will take effect after the outermost [`dispatch()`](#dispatch) call on the stack exits. This means that if you subscribe or unsubscribe while listeners are being invoked, the changes to the subscriptions will take effect only after the outermost [`dispatch()`](#dispatch) exits.
87+
1. The subscriptions are snapshotted just before every [`dispatch()`](#dispatch) call. If you subscribe or unsubscribe while the listeners are being invoked, this will not have any effect on the [`dispatch()`](#dispatch) that is currently in progress. However, the next [`dispatch()`](#dispatch) call, whether nested or not, will use a more recent snapshot of the subscription list.
8888

89-
2. The listener should not expect to see all states changes, as the state might have been updated multiple times during a nested [`dispatch()`](#dispatch) before the listener is called. It is, however, guaranteed that all subscribers registered by the time the outermost [`dispatch()`](#dispatch) started will be called with the latest state by the time the outermost [`dispatch()`](#dispatch) exits.
89+
2. The listener should not expect to see all states changes, as the state might have been updated multiple times during a nested [`dispatch()`](#dispatch) before the listener is called. It is, however, guaranteed that all subscribers registered before the [`dispatch()`](#dispatch) started will be called with the latest state by the time it exits.
9090

9191
It is a low-level API. Most likely, instead of using it directly, you’ll use React (or other) bindings. If you feel that the callback needs to be invoked with the current state, you might want to [convert the store to an Observable or write a custom `observeStore` utility instead](https://github.com/rackt/redux/issues/303#issuecomment-125184409).
9292

src/createStore.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,21 @@ export default function createStore(reducer, initialState, enhancer) {
7171
* Adds a change listener. It will be called any time an action is dispatched,
7272
* and some part of the state tree may potentially have changed. You may then
7373
* call `getState()` to read the current state tree inside the callback.
74-
* Note, the listener should not expect to see all states changes, as the
75-
* state might have been updated multiple times before the listener is
76-
* notified.
74+
*
75+
* You may call `dispatch()` from a change listener, with the following
76+
* caveats:
77+
*
78+
* 1. The subscriptions are snapshotted just before every `dispatch()` call.
79+
* If you subscribe or unsubscribe while the listeners are being invoked, this
80+
* will not have any effect on the `dispatch()` that is currently in progress.
81+
* However, the next `dispatch()` call, whether nested or not, will use a more
82+
* recent snapshot of the subscription list.
83+
*
84+
* 2. The listener should not expect to see all states changes, as the state
85+
* might have been updated multiple times during a nested `dispatch()` before
86+
* the listener is called. It is, however, guaranteed that all subscribers
87+
* registered before the `dispatch()` started will be called with the latest
88+
* state by the time it exits.
7789
*
7890
* @param {Function} listener A callback to be invoked on every dispatch.
7991
* @returns {Function} A function to remove this change listener.

test/createStore.spec.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,48 @@ describe('createStore', () => {
348348
expect(listener3.calls.length).toBe(1)
349349
})
350350

351+
it('uses the last snapshot of subscribers during nested dispatch', () => {
352+
const store = createStore(reducers.todos)
353+
354+
const listener1 = expect.createSpy(() => {})
355+
const listener2 = expect.createSpy(() => {})
356+
const listener3 = expect.createSpy(() => {})
357+
const listener4 = expect.createSpy(() => {})
358+
359+
let unsubscribe4
360+
const unsubscribe1 = store.subscribe(() => {
361+
listener1()
362+
expect(listener1.calls.length).toBe(1)
363+
expect(listener2.calls.length).toBe(0)
364+
expect(listener3.calls.length).toBe(0)
365+
expect(listener4.calls.length).toBe(0)
366+
367+
unsubscribe1()
368+
unsubscribe4 = store.subscribe(listener4)
369+
store.dispatch(unknownAction())
370+
371+
expect(listener1.calls.length).toBe(1)
372+
expect(listener2.calls.length).toBe(1)
373+
expect(listener3.calls.length).toBe(1)
374+
expect(listener4.calls.length).toBe(1)
375+
})
376+
const unsubscribe2 = store.subscribe(listener2)
377+
const unsubscribe3 = store.subscribe(listener3)
378+
379+
store.dispatch(unknownAction())
380+
expect(listener1.calls.length).toBe(1)
381+
expect(listener2.calls.length).toBe(2)
382+
expect(listener3.calls.length).toBe(2)
383+
expect(listener4.calls.length).toBe(1)
384+
385+
unsubscribe4()
386+
store.dispatch(unknownAction())
387+
expect(listener1.calls.length).toBe(1)
388+
expect(listener2.calls.length).toBe(3)
389+
expect(listener3.calls.length).toBe(3)
390+
expect(listener4.calls.length).toBe(1)
391+
})
392+
351393
it('provides an up-to-date state when a subscriber is notified', done => {
352394
const store = createStore(reducers.todos)
353395
store.subscribe(() => {

0 commit comments

Comments
 (0)