Skip to content

Commit 271d6d2

Browse files
committed
Merge remote-tracking branch 'upstream/next' into patch-1
2 parents 7216abc + 4acb40c commit 271d6d2

22 files changed

+5263
-71
lines changed

docs/advanced/Middleware.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,12 +265,16 @@ The implementation of [`applyMiddleware()`](../api/applyMiddleware.md) that ship
265265

266266
* It only exposes a subset of the [store API](../api/Store.md) to the middleware: [`dispatch(action)`](../api/Store.md#dispatch) and [`getState()`](../api/Store.md#getState).
267267

268-
* It does a bit of trickery to make sure that if you call `store.dispatch(action)` from your middleware instead of `next(action)`, the action will actually travel the whole middleware chain again, including the current middleware. This is useful for asynchronous middleware, as we have seen [previously](AsyncActions.md).
268+
* It does a bit of trickery to make sure that if you call `store.dispatch(action)` from your middleware instead of `next(action)`, the action will actually travel the whole middleware chain again, including the current middleware. This is useful for asynchronous middleware, as we have seen [previously](AsyncActions.md). There is one caveat when calling `dispatch` during setup, described below.
269269

270270
* To ensure that you may only apply middleware once, it operates on `createStore()` rather than on `store` itself. Instead of `(store, middlewares) => store`, its signature is `(...middlewares) => (createStore) => createStore`.
271271

272272
Because it is cumbersome to apply functions to `createStore()` before using it, `createStore()` accepts an optional last argument to specify such functions.
273273

274+
#### Caveat: Dispatching During Setup
275+
276+
While `applyMiddleware` executes and sets up your middleware, the `store.dispatch` function will point to the vanilla version provided by `createStore`. Dispatching would result in no other middleware being applied. If you are expecting an interaction with another middleware during setup, you will probably be disappointed. Because of this unexpected behavior, `applyMiddleware` will throw an error if you try to dispatch an action before the set up completes. Instead, you should either communicate directly with that other middleware via a common object (for an API-calling middleware, this may be your API client object) or waiting until after the middleware is constructed with a callback.
277+
274278
### The Final Approach
275279

276280
Given this middleware we just wrote:

index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export interface Action<T = any> {
4646
* @template S The type of state consumed and produced by this reducer.
4747
* @template A The type of actions the reducer can potentially respond to.
4848
*/
49-
export type Reducer<S = {}, A extends Action = Action> = (state: S, action: A) => S;
49+
export type Reducer<S = {}, A extends Action = Action> = (state: S | undefined, action: A) => S;
5050

5151
/**
5252
* Object whose values correspond to different reducer functions.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
"description": "Predictable state container for JavaScript apps",
55
"main": "lib/index.js",
66
"module": "es/index.js",
7-
"jsnext:main": "es/index.js",
87
"typings": "./index.d.ts",
98
"files": [
109
"dist",

src/applyMiddleware.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ import compose from './compose'
1919
export default function applyMiddleware(...middlewares) {
2020
return (createStore) => (reducer, preloadedState, enhancer) => {
2121
const store = createStore(reducer, preloadedState, enhancer)
22-
let dispatch = store.dispatch
22+
let dispatch = () => {
23+
throw new Error(
24+
`Dispatching while constructing your middleware is not allowed. ` +
25+
`Other middleware would not be applied to this dispatch.`
26+
)
27+
}
2328
let chain = []
2429

2530
const middlewareAPI = {

src/combineReducers.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ActionTypes } from './createStore'
1+
import ActionTypes from './utils/actionTypes'
22
import isPlainObject from 'lodash/isPlainObject'
33
import warning from './utils/warning'
44

src/createStore.js

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
import isPlainObject from 'lodash/isPlainObject'
22
import $$observable from 'symbol-observable'
33

4-
/**
5-
* These are private action types reserved by Redux.
6-
* For any unknown actions, you must return the current state.
7-
* If the current state is undefined, you must return the initial state.
8-
* Do not reference these action types directly in your code.
9-
*/
10-
export const ActionTypes = {
11-
INIT: '@@redux/INIT'
12-
}
4+
import ActionTypes from './utils/actionTypes'
135

146
/**
157
* Creates a Redux store that holds the state tree.
@@ -72,6 +64,14 @@ export default function createStore(reducer, preloadedState, enhancer) {
7264
* @returns {any} The current state tree of your application.
7365
*/
7466
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+
7575
return currentState
7676
}
7777

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

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+
106115
let isSubscribed = true
107116

108117
ensureCanMutateNextListeners()
@@ -113,6 +122,13 @@ export default function createStore(reducer, preloadedState, enhancer) {
113122
return
114123
}
115124

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+
116132
isSubscribed = false
117133

118134
ensureCanMutateNextListeners()

src/utils/actionTypes.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* These are private action types reserved by Redux.
3+
* For any unknown actions, you must return the current state.
4+
* If the current state is undefined, you must return the initial state.
5+
* Do not reference these action types directly in your code.
6+
*/
7+
var ActionTypes = {
8+
INIT: '@@redux/INIT'
9+
}
10+
11+
export default ActionTypes

test/applyMiddleware.spec.js

Lines changed: 11 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ import { addTodo, addTodoAsync, addTodoIfEmpty } from './helpers/actionCreators'
44
import { thunk } from './helpers/middleware'
55

66
describe('applyMiddleware', () => {
7+
it('warns when dispatching during middleware setup', () => {
8+
function dispatchingMiddleware(store) {
9+
store.dispatch(addTodo('Dont dispatch in middleware setup'))
10+
return next => action => next(action)
11+
}
12+
13+
expect(() =>
14+
applyMiddleware(dispatchingMiddleware)(createStore)(reducers.todos)
15+
).toThrow()
16+
})
17+
718
it('wraps dispatch method with middleware once', () => {
819
function test(spyOnMethods) {
920
return methods => {
@@ -91,42 +102,4 @@ describe('applyMiddleware', () => {
91102
done()
92103
})
93104
})
94-
95-
it('passes through all arguments of dispatch calls from within middleware', () => {
96-
const spy = jest.fn()
97-
const testCallArgs = ['test']
98-
function multiArgMiddleware() {
99-
return next => (action, callArgs) => {
100-
if (Array.isArray(callArgs)) {
101-
return action(...callArgs)
102-
}
103-
return next(action)
104-
}
105-
}
106-
function dummyMiddleware({ dispatch }) {
107-
return next => action => dispatch(action, testCallArgs)
108-
}
109-
110-
const store = createStore(reducers.todos, applyMiddleware(multiArgMiddleware, dummyMiddleware))
111-
store.dispatch(spy)
112-
expect(spy.mock.calls[0]).toEqual(testCallArgs)
113-
})
114-
115-
it('keeps unwrapped dispatch available while middleware is initializing', () => {
116-
// This is documenting the existing behavior in Redux 3.x.
117-
// We plan to forbid this in Redux 4.x.
118-
119-
function earlyDispatch({ dispatch }) {
120-
dispatch(addTodo('Hello'))
121-
return () => action => action
122-
}
123-
124-
const store = createStore(reducers.todos, applyMiddleware(earlyDispatch))
125-
expect(store.getState()).toEqual([
126-
{
127-
id: 1,
128-
text: 'Hello'
129-
}
130-
])
131-
})
132105
})

test/combineReducers.spec.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable no-console */
22
import { combineReducers } from '../src'
3-
import createStore, { ActionTypes } from '../src/createStore'
3+
import createStore from '../src/createStore'
4+
import ActionTypes from '../src/utils/actionTypes'
45

56
describe('Utils', () => {
67
describe('combineReducers', () => {

test/createStore.spec.js

Lines changed: 34 additions & 1 deletion
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

Lines changed: 30 additions & 1 deletion
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 38 additions & 1 deletion
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:

test/typescript.spec.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ describe('TypeScript definitions', function () {
66
tt.compileDirectory(
77
__dirname + '/typescript',
88
fileName => fileName.match(/\.ts$/),
9+
{
10+
strictNullChecks: true
11+
},
912
() => done()
1013
)
1114
})

test/typescript/actionCreators.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
ActionCreator, Action, Dispatch,
33
bindActionCreators, ActionCreatorsMapObject
4-
} from "../../index";
4+
} from "../../"
55

66

77
interface AddTodoAction extends Action {

test/typescript/actions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Action as ReduxAction} from "../../index";
1+
import {Action as ReduxAction} from "../../"
22

33

44
namespace FSA {

test/typescript/compose.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {compose} from "../../index";
1+
import {compose} from "../../"
22

33
// copied from DefinitelyTyped/compose-function
44

test/typescript/dispatch.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import {Dispatch, Action} from "../../index";
1+
import {Dispatch, Action} from "../../"
22

33

44
declare const dispatch: Dispatch<any>;
55

66
const dispatchResult: Action = dispatch({type: 'TYPE'});
77

88
// thunk
9-
declare module "../../index" {
9+
declare module "../../" {
1010
export interface Dispatch<S> {
1111
<R>(asyncAction: (dispatch: Dispatch<S>, getState: () => S) => R): R;
1212
}

0 commit comments

Comments
 (0)