Skip to content

Commit c6c0d69

Browse files
mwilc0xtimdorr
authored andcommitted
throw if getState, subscribe, or unsubscribe called while dispatching (reduxjs#1569)
* throw error if getState, subscribe, or unsubscribe called while dispatching * prevent throwing if not subscribed * update getState error message * fix space after period * update subscribe/unsubscribe error messages
1 parent b62248b commit c6c0d69

File tree

5 files changed

+129
-3
lines changed

5 files changed

+129
-3
lines changed

src/createStore.js

+24
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ export default function createStore(reducer, preloadedState, enhancer) {
6464
* @returns {any} The current state tree of your application.
6565
*/
6666
function getState() {
67+
if (isDispatching) {
68+
throw new Error(
69+
'You may not call store.getState() while the reducer is executing. ' +
70+
'The reducer has already received the state as an argument. ' +
71+
'Pass it down from the top reducer instead of reading it from the store.'
72+
)
73+
}
74+
6775
return currentState
6876
}
6977

@@ -95,6 +103,15 @@ export default function createStore(reducer, preloadedState, enhancer) {
95103
throw new Error('Expected listener to be a function.')
96104
}
97105

106+
if (isDispatching) {
107+
throw new Error(
108+
'You may not call store.subscribe() while the reducer is executing. ' +
109+
'If you would like to be notified after the store has been updated, subscribe from a ' +
110+
'component and invoke store.getState() in the callback to access the latest state. ' +
111+
'See http://redux.js.org/docs/api/Store.html#subscribe for more details.'
112+
)
113+
}
114+
98115
let isSubscribed = true
99116

100117
ensureCanMutateNextListeners()
@@ -105,6 +122,13 @@ export default function createStore(reducer, preloadedState, enhancer) {
105122
return
106123
}
107124

125+
if (isDispatching) {
126+
throw new Error(
127+
'You may not unsubscribe from a store listener while the reducer is executing. ' +
128+
'See http://redux.js.org/docs/api/Store.html#subscribe for more details.'
129+
)
130+
}
131+
108132
isSubscribed = false
109133

110134
ensureCanMutateNextListeners()

test/createStore.spec.js

+34-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import { createStore, combineReducers } from '../src/index'
2-
import { addTodo, dispatchInMiddle, throwError, unknownAction } from './helpers/actionCreators'
2+
import {
3+
addTodo,
4+
dispatchInMiddle,
5+
getStateInMiddle,
6+
subscribeInMiddle,
7+
unsubscribeInMiddle,
8+
throwError,
9+
unknownAction
10+
} from './helpers/actionCreators'
311
import * as reducers from './helpers/reducers'
412
import * as Rx from 'rxjs'
513
import $$observable from 'symbol-observable'
@@ -461,6 +469,31 @@ describe('createStore', () => {
461469
).toThrow(/may not dispatch/)
462470
})
463471

472+
it('does not allow getState() from within a reducer', () => {
473+
const store = createStore(reducers.getStateInTheMiddleOfReducer)
474+
475+
expect(() =>
476+
store.dispatch(getStateInMiddle(store.getState.bind(store)))
477+
).toThrow(/You may not call store.getState()/)
478+
})
479+
480+
it('does not allow subscribe() from within a reducer', () => {
481+
const store = createStore(reducers.subscribeInTheMiddleOfReducer)
482+
483+
expect(() =>
484+
store.dispatch(subscribeInMiddle(store.subscribe.bind(store, () => {})))
485+
).toThrow(/You may not call store.subscribe()/)
486+
})
487+
488+
it('does not allow unsubscribe from subscribe() from within a reducer', () => {
489+
const store = createStore(reducers.unsubscribeInTheMiddleOfReducer)
490+
const unsubscribe = store.subscribe(() => {})
491+
492+
expect(() =>
493+
store.dispatch(unsubscribeInMiddle(unsubscribe.bind(store)))
494+
).toThrow(/You may not unsubscribe from a store/)
495+
})
496+
464497
it('recovers from an error within a reducer', () => {
465498
const store = createStore(reducers.errorThrowingReducer)
466499
expect(() =>

test/helpers/actionCreators.js

+30-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { ADD_TODO, DISPATCH_IN_MIDDLE, THROW_ERROR, UNKNOWN_ACTION } from './actionTypes'
1+
import {
2+
ADD_TODO,
3+
DISPATCH_IN_MIDDLE,
4+
GET_STATE_IN_MIDDLE,
5+
SUBSCRIBE_IN_MIDDLE,
6+
UNSUBSCRIBE_IN_MIDDLE,
7+
THROW_ERROR,
8+
UNKNOWN_ACTION
9+
} from './actionTypes'
210

311
export function addTodo(text) {
412
return { type: ADD_TODO, text }
@@ -26,6 +34,27 @@ export function dispatchInMiddle(boundDispatchFn) {
2634
}
2735
}
2836

37+
export function getStateInMiddle(boundGetStateFn) {
38+
return {
39+
type: GET_STATE_IN_MIDDLE,
40+
boundGetStateFn
41+
}
42+
}
43+
44+
export function subscribeInMiddle(boundSubscribeFn) {
45+
return {
46+
type: SUBSCRIBE_IN_MIDDLE,
47+
boundSubscribeFn
48+
}
49+
}
50+
51+
export function unsubscribeInMiddle(boundUnsubscribeFn) {
52+
return {
53+
type: UNSUBSCRIBE_IN_MIDDLE,
54+
boundUnsubscribeFn
55+
}
56+
}
57+
2958
export function throwError() {
3059
return {
3160
type: THROW_ERROR

test/helpers/actionTypes.js

+3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
export const ADD_TODO = 'ADD_TODO'
22
export const DISPATCH_IN_MIDDLE = 'DISPATCH_IN_MIDDLE'
3+
export const GET_STATE_IN_MIDDLE = 'GET_STATE_IN_MIDDLE'
4+
export const SUBSCRIBE_IN_MIDDLE = 'SUBSCRIBE_IN_MIDDLE'
5+
export const UNSUBSCRIBE_IN_MIDDLE = 'UNSUBSCRIBE_IN_MIDDLE'
36
export const THROW_ERROR = 'THROW_ERROR'
47
export const UNKNOWN_ACTION = 'UNKNOWN_ACTION'

test/helpers/reducers.js

+38-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { ADD_TODO, DISPATCH_IN_MIDDLE, THROW_ERROR } from './actionTypes'
1+
import {
2+
ADD_TODO,
3+
DISPATCH_IN_MIDDLE,
4+
GET_STATE_IN_MIDDLE,
5+
SUBSCRIBE_IN_MIDDLE,
6+
UNSUBSCRIBE_IN_MIDDLE,
7+
THROW_ERROR
8+
} from './actionTypes'
29

310

411
function id(state = []) {
@@ -46,6 +53,36 @@ export function dispatchInTheMiddleOfReducer(state = [], action) {
4653
}
4754
}
4855

56+
export function getStateInTheMiddleOfReducer(state = [], action) {
57+
switch (action.type) {
58+
case GET_STATE_IN_MIDDLE:
59+
action.boundGetStateFn()
60+
return state
61+
default:
62+
return state
63+
}
64+
}
65+
66+
export function subscribeInTheMiddleOfReducer(state = [], action) {
67+
switch (action.type) {
68+
case SUBSCRIBE_IN_MIDDLE:
69+
action.boundSubscribeFn()
70+
return state
71+
default:
72+
return state
73+
}
74+
}
75+
76+
export function unsubscribeInTheMiddleOfReducer(state = [], action) {
77+
switch (action.type) {
78+
case UNSUBSCRIBE_IN_MIDDLE:
79+
action.boundUnsubscribeFn()
80+
return state
81+
default:
82+
return state
83+
}
84+
}
85+
4986
export function errorThrowingReducer(state = [], action) {
5087
switch (action.type) {
5188
case THROW_ERROR:

0 commit comments

Comments
 (0)