Skip to content

Commit

Permalink
Fix documentation around nested dispatches and add test to verify it
Browse files Browse the repository at this point in the history
  • Loading branch information
gaearon committed Jan 31, 2016
1 parent cdb296c commit 24aade4
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 5 deletions.
4 changes: 2 additions & 2 deletions docs/api/Store.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,9 @@ Adds a change listener. It will be called any time an action is dispatched, and

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

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.
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.

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.
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.

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).

Expand Down
18 changes: 15 additions & 3 deletions src/createStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,21 @@ export default function createStore(reducer, initialState, enhancer) {
* Adds a change listener. It will be called any time an action is dispatched,
* and some part of the state tree may potentially have changed. You may then
* call `getState()` to read the current state tree inside the callback.
* Note, the listener should not expect to see all states changes, as the
* state might have been updated multiple times before the listener is
* notified.
*
* You may call `dispatch()` from a change listener, with the following
* caveats:
*
* 1. The subscriptions are snapshotted just before every `dispatch()` call.
* If you subscribe or unsubscribe while the listeners are being invoked, this
* will not have any effect on the `dispatch()` that is currently in progress.
* However, the next `dispatch()` call, whether nested or not, will use a more
* recent snapshot of the subscription list.
*
* 2. The listener should not expect to see all states changes, as the state
* might have been updated multiple times during a nested `dispatch()` before
* the listener is called. It is, however, guaranteed that all subscribers
* registered before the `dispatch()` started will be called with the latest
* state by the time it exits.
*
* @param {Function} listener A callback to be invoked on every dispatch.
* @returns {Function} A function to remove this change listener.
Expand Down
42 changes: 42 additions & 0 deletions test/createStore.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,48 @@ describe('createStore', () => {
expect(listener3.calls.length).toBe(1)
})

it('uses the last snapshot of subscribers during nested dispatch', () => {
const store = createStore(reducers.todos)

const listener1 = expect.createSpy(() => {})
const listener2 = expect.createSpy(() => {})
const listener3 = expect.createSpy(() => {})
const listener4 = expect.createSpy(() => {})

let unsubscribe4
const unsubscribe1 = store.subscribe(() => {
listener1()
expect(listener1.calls.length).toBe(1)
expect(listener2.calls.length).toBe(0)
expect(listener3.calls.length).toBe(0)
expect(listener4.calls.length).toBe(0)

unsubscribe1()
unsubscribe4 = store.subscribe(listener4)
store.dispatch(unknownAction())

expect(listener1.calls.length).toBe(1)
expect(listener2.calls.length).toBe(1)
expect(listener3.calls.length).toBe(1)
expect(listener4.calls.length).toBe(1)
})
const unsubscribe2 = store.subscribe(listener2)
const unsubscribe3 = store.subscribe(listener3)

store.dispatch(unknownAction())
expect(listener1.calls.length).toBe(1)
expect(listener2.calls.length).toBe(2)
expect(listener3.calls.length).toBe(2)
expect(listener4.calls.length).toBe(1)

unsubscribe4()
store.dispatch(unknownAction())
expect(listener1.calls.length).toBe(1)
expect(listener2.calls.length).toBe(3)
expect(listener3.calls.length).toBe(3)
expect(listener4.calls.length).toBe(1)
})

it('provides an up-to-date state when a subscriber is notified', done => {
const store = createStore(reducers.todos)
store.subscribe(() => {
Expand Down

0 comments on commit 24aade4

Please sign in to comment.