diff --git a/docs/api/Store.md b/docs/api/Store.md index 4713798434..52c12ae073 100644 --- a/docs/api/Store.md +++ b/docs/api/Store.md @@ -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). diff --git a/src/createStore.js b/src/createStore.js index e68fa3dfc6..53a2b9dbf5 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -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. diff --git a/test/createStore.spec.js b/test/createStore.spec.js index d4c4b1af37..988b58fefb 100644 --- a/test/createStore.spec.js +++ b/test/createStore.spec.js @@ -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(() => {