Skip to content

Commit 659ff14

Browse files
authored
Enable customizing default middleware and store enhancers (#192)
* Extract getDefaultMiddleware to its own file * Bump immutable middleware types * Rewrite typings tests to drop fake import * Implement customizing default middleware - Added ability to enable/disable default middleware via flags - Added passing options to default middleware * Add unit tests for middleware options * Add ignoredActions argument to serializable middleware * Update default middleware docs * Add ability to customize store enhancer order. * Rename EnhancerOptions to DevToolsOptions * Update configureStore docs with TS types and enhancer callback info * Fix typo
1 parent 10e0e94 commit 659ff14

16 files changed

+451
-113
lines changed

docs/api/configureStore.md

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,62 @@ hide_title: true
77

88
# `configureStore`
99

10-
A friendlier abstraction over the standard Redux `createStore` function.
10+
A friendly abstraction over the standard Redux `createStore` function that adds good defaults
11+
to the store setup for a better development experience.
1112

1213
## Parameters
1314

1415
`configureStore` accepts a single configuration object parameter, with the following options:
1516

1617
```ts
17-
function configureStore({
18-
// A single reducer function that will be used as the root reducer,
19-
// or an object of slice reducers that will be passed to combineReducers()
20-
reducer: Object<string, ReducerFunction> | ReducerFunction,
21-
// An array of Redux middlewares. If not supplied, uses getDefaultMiddleware()
22-
middleware?: MiddlewareFunction[],
23-
// Enable support for the Redux DevTools Extension. Defaults to true.
24-
devTools?: boolean | EnhancerOptions,
25-
// Same as current createStore.
26-
preloadedState?: State,
27-
// An optional array of Redux store enhancers
28-
enhancers?: ReduxStoreEnhancer[],
29-
})
18+
type ConfigureEnhancersCallback = (
19+
defaultEnhancers: StoreEnhancer[]
20+
) => StoreEnhancer[]
21+
22+
interface ConfigureStoreOptions<S = any, A extends Action = AnyAction> {
23+
/**
24+
* A single reducer function that will be used as the root reducer, or an
25+
* object of slice reducers that will be passed to `combineReducers()`.
26+
*/
27+
reducer: Reducer<S, A> | ReducersMapObject<S, A>
28+
29+
/**
30+
* An array of Redux middleware to install. If not supplied, defaults to
31+
* the set of middleware returned by `getDefaultMiddleware()`.
32+
*/
33+
middleware?: Middleware<{}, S>[]
34+
35+
/**
36+
* Whether to enable Redux DevTools integration. Defaults to `true`.
37+
*
38+
* Additional configuration can be done by passing Redux DevTools options
39+
*/
40+
devTools?: boolean | DevToolsOptions
41+
42+
/**
43+
* The initial state, same as Redux's createStore.
44+
* You may optionally specify it to hydrate the state
45+
* from the server in universal apps, or to restore a previously serialized
46+
* user session. If you use `combineReducers()` to produce the root reducer
47+
* function (either directly or indirectly by passing an object as `reducer`),
48+
* this must be an object with the same shape as the reducer map keys.
49+
*/
50+
preloadedState?: DeepPartial<S extends any ? S : S>
51+
52+
/**
53+
* The store enhancers to apply. See Redux's `createStore()`.
54+
* All enhancers will be included before the DevTools Extension enhancer.
55+
* If you need to customize the order of enhancers, supply a callback
56+
* function that will receive the original array (ie, `[applyMiddleware]`),
57+
* and should return a new array (such as `[applyMiddleware, offline]`).
58+
* If you only need to add middleware, use the `middleware` parameter instead.
59+
*/
60+
enhancers?: StoreEnhancer[] | ConfigureEnhancersCallback
61+
}
62+
63+
function configureStore<S = any, A extends Action = AnyAction>(
64+
options: ConfigureStoreOptions<S, A>
65+
): EnhancedStore<S, A>
3066
```
3167

3268
### `reducer`
@@ -70,10 +106,20 @@ An optional initial state value to be passed to the Redux `createStore` function
70106

71107
### `enhancers`
72108

73-
An optional array of Redux store enhancers. If included, these will be passed to [the Redux `compose` function](https://redux.js.org/api/compose), and the combined enhancer will be passed to `createStore`.
109+
An optional array of Redux store enhancers, or a callback function to customize the array of enhancers.
110+
111+
If defined as an array, these will be passed to [the Redux `compose` function](https://redux.js.org/api/compose), and the combined enhancer will be passed to `createStore`.
112+
113+
This should _not_ include `applyMiddleware()` or the Redux DevTools Extension `composeWithDevTools`, as those are already handled by `configureStore`.
114+
115+
Example: `enhancers: [offline]` will result in a final setup of `[applyMiddleware, offline, devToolsExtension]`.
116+
117+
If defined as a callback function, it will be called with the existing array of enhancers _without_ the DevTools Extension (currently `[applyMiddleware]`),
118+
and should return a new array of enhancers. This is primarily useful for cases where a store enhancer needs to be added
119+
in front of `applyMiddleware`, such as `redux-first-router` or `redux-offline`.
74120

75-
This should _not_ include `applyMiddleware()` or
76-
the Redux DevTools Extension `composeWithDevTools`, as those are already handled by `configureStore`.
121+
Example: `enhancers: (defaultEnhancers) => [offline, ...defaultEnhancers]` will result in a final setup
122+
of `[offline, applyMiddleware, devToolsExtension]`.
77123
78124
## Usage
79125

docs/api/getDefaultMiddleware.md

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,75 @@ by default, since thunks are the basic recommended side effects middleware for R
7070
Currently, the return value is:
7171

7272
```js
73-
;[immutableStateInvariant, thunk, serializableStateInvariant]
73+
const middleware = [thunk, immutableStateInvariant, serializableStateInvariant]
7474
```
7575

7676
### Production
7777

7878
Currently, the return value is:
7979

8080
```js
81-
;[thunk]
81+
const middleware = [thunk]
82+
```
83+
84+
## Customizing the Included Middleware
85+
86+
`getDefaultMiddleware` accepts an options object that allows customizing each middleware in two ways:
87+
88+
- Each middleware can be excluded from inclusion in the array by passing `false` for its corresponding field
89+
- Each middleware can have its options customized by passing the matching options object for its corresponding field
90+
91+
This example shows excluding the serializable state check middleware, and passing a specific value for the thunk
92+
middleware's "extra argument":
93+
94+
```ts
95+
const customizedMiddleware = getDefaultMiddleware({
96+
thunk: {
97+
extraArgument: myCustomApiService
98+
},
99+
serializableCheck: false
100+
})
101+
```
102+
103+
## API Reference
104+
105+
```ts
106+
interface ThunkOptions<E = any> {
107+
extraArgument: E
108+
}
109+
110+
interface ImmutableStateInvariantMiddlewareOptions {
111+
isImmutable?: (value: any) => boolean
112+
ignore?: string[]
113+
}
114+
115+
interface SerializableStateInvariantMiddlewareOptions {
116+
/**
117+
* The function to check if a value is considered serializable. This
118+
* function is applied recursively to every value contained in the
119+
* state. Defaults to `isPlain()`.
120+
*/
121+
isSerializable?: (value: any) => boolean
122+
/**
123+
* The function that will be used to retrieve entries from each
124+
* value. If unspecified, `Object.entries` will be used. Defaults
125+
* to `undefined`.
126+
*/
127+
getEntries?: (value: any) => [string, any][]
128+
129+
/**
130+
* An array of action types to ignore when checking for serializability, Defaults to []
131+
*/
132+
ignoredActions?: string[]
133+
}
134+
135+
interface GetDefaultMiddlewareOptions {
136+
thunk?: boolean | ThunkOptions
137+
immutableCheck?: boolean | ImmutableStateInvariantMiddlewareOptions
138+
serializableCheck?: boolean | SerializableStateInvariantMiddlewareOptions
139+
}
140+
141+
function getDefaultMiddleware<S = any>(
142+
options: GetDefaultMiddlewareOptions = {}
143+
): Middleware<{}, S>[]
82144
```

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"@babel/preset-typescript": "^7.3.3",
1616
"@types/jest": "^24.0.11",
1717
"@types/node": "^10.14.4",
18-
"@types/redux-immutable-state-invariant": "^2.1.0",
18+
"@types/redux-immutable-state-invariant": "^2.1.1",
1919
"@typescript-eslint/parser": "^1.6.0",
2020
"babel-eslint": "^10.0.1",
2121
"eslint": "^5.16.0",

src/configureStore.test.ts

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,13 @@
1-
import { configureStore, getDefaultMiddleware } from './configureStore'
1+
import { configureStore } from './configureStore'
22
import * as redux from 'redux'
33
import * as devtools from 'redux-devtools-extension'
4-
5-
import thunk from 'redux-thunk'
6-
7-
describe('getDefaultMiddleware', () => {
8-
const ORIGINAL_NODE_ENV = process.env.NODE_ENV
9-
10-
afterEach(() => {
11-
process.env.NODE_ENV = ORIGINAL_NODE_ENV
12-
})
13-
14-
it('returns an array with only redux-thunk in production', () => {
15-
process.env.NODE_ENV = 'production'
16-
17-
expect(getDefaultMiddleware()).toEqual([thunk])
18-
})
19-
20-
it('returns an array with additional middleware in development', () => {
21-
const middleware = getDefaultMiddleware()
22-
expect(middleware).toContain(thunk)
23-
expect(middleware.length).toBeGreaterThan(1)
24-
})
25-
})
4+
import {
5+
StoreCreator,
6+
StoreEnhancer,
7+
StoreEnhancerStoreCreator,
8+
Reducer,
9+
AnyAction
10+
} from 'redux'
2611

2712
describe('configureStore', () => {
2813
jest.spyOn(redux, 'applyMiddleware')
@@ -166,5 +151,28 @@ describe('configureStore', () => {
166151
expect.any(Function)
167152
)
168153
})
154+
155+
it('accepts a callback for customizing enhancers', () => {
156+
let dummyEnhancerCalled = false
157+
158+
const dummyEnhancer: StoreEnhancer = (
159+
createStore: StoreEnhancerStoreCreator
160+
) => (reducer, ...args: any[]) => {
161+
dummyEnhancerCalled = true
162+
163+
return createStore(reducer, ...args)
164+
}
165+
166+
const reducer = () => ({})
167+
168+
const store = configureStore({
169+
reducer,
170+
enhancers: defaultEnhancers => {
171+
return [...defaultEnhancers, dummyEnhancer]
172+
}
173+
})
174+
175+
expect(dummyEnhancerCalled).toBe(true)
176+
})
169177
})
170178
})

src/configureStore.ts

Lines changed: 28 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,42 +12,20 @@ import {
1212
Store,
1313
DeepPartial
1414
} from 'redux'
15-
import { composeWithDevTools, EnhancerOptions } from 'redux-devtools-extension'
16-
import thunk, { ThunkDispatch, ThunkMiddleware } from 'redux-thunk'
17-
18-
// UMD-DEV-ONLY: import createImmutableStateInvariantMiddleware from 'redux-immutable-state-invariant'
19-
20-
import { createSerializableStateInvariantMiddleware } from './serializableStateInvariantMiddleware'
15+
import {
16+
composeWithDevTools,
17+
EnhancerOptions as DevToolsOptions
18+
} from 'redux-devtools-extension'
19+
import { ThunkDispatch } from 'redux-thunk'
2120

2221
import isPlainObject from './isPlainObject'
22+
import { getDefaultMiddleware } from './getDefaultMiddleware'
2323

2424
const IS_PRODUCTION = process.env.NODE_ENV === 'production'
2525

26-
/**
27-
* Returns any array containing the default middleware installed by
28-
* `configureStore()`. Useful if you want to configure your store with a custom
29-
* `middleware` array but still keep the default set.
30-
*
31-
* @return The default middleware used by `configureStore()`.
32-
*/
33-
export function getDefaultMiddleware<S = any, A extends Action = AnyAction>(): [
34-
ThunkMiddleware<S, A>,
35-
...Middleware<{}, S>[]
36-
] {
37-
let middlewareArray: [ThunkMiddleware<S, A>, ...Middleware<{}, S>[]] = [thunk]
38-
39-
if (process.env.NODE_ENV !== 'production') {
40-
/* START_REMOVE_UMD */
41-
const createImmutableStateInvariantMiddleware = require('redux-immutable-state-invariant')
42-
.default
43-
middlewareArray.unshift(createImmutableStateInvariantMiddleware())
44-
/* STOP_REMOVE_UMD */
45-
46-
middlewareArray.push(createSerializableStateInvariantMiddleware())
47-
}
48-
49-
return middlewareArray
50-
}
26+
export type ConfigureEnhancersCallback = (
27+
defaultEnhancers: StoreEnhancer[]
28+
) => StoreEnhancer[]
5129

5230
/**
5331
* Options for `configureStore()`.
@@ -68,12 +46,13 @@ export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction> {
6846
/**
6947
* Whether to enable Redux DevTools integration. Defaults to `true`.
7048
*
71-
* Additional configuration can be done by passing enhancer options
49+
* Additional configuration can be done by passing Redux DevTools options
7250
*/
73-
devTools?: boolean | EnhancerOptions
51+
devTools?: boolean | DevToolsOptions
7452

7553
/**
76-
* The initial state. You may optionally specify it to hydrate the state
54+
* The initial state, same as Redux's createStore.
55+
* You may optionally specify it to hydrate the state
7756
* from the server in universal apps, or to restore a previously serialized
7857
* user session. If you use `combineReducers()` to produce the root reducer
7958
* function (either directly or indirectly by passing an object as `reducer`),
@@ -86,10 +65,14 @@ export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction> {
8665
preloadedState?: DeepPartial<S extends any ? S : S>
8766

8867
/**
89-
* The store enhancers to apply. See Redux's `createStore()`. If you only
90-
* need to add middleware, you can use the `middleware` parameter instaead.
68+
* The store enhancers to apply. See Redux's `createStore()`.
69+
* All enhancers will be included before the DevTools Extension enhancer.
70+
* If you need to customize the order of enhancers, supply a callback
71+
* function that will receive the original array (ie, `[applyMiddleware]`),
72+
* and should return a new array (such as `[applyMiddleware, offline]`).
73+
* If you only need to add middleware, you can use the `middleware` parameter instaead.
9174
*/
92-
enhancers?: StoreEnhancer[]
75+
enhancers?: StoreEnhancer[] | ConfigureEnhancersCallback
9376
}
9477

9578
/**
@@ -115,7 +98,7 @@ export function configureStore<S = any, A extends Action = AnyAction>(
11598
middleware = getDefaultMiddleware(),
11699
devTools = true,
117100
preloadedState = undefined,
118-
enhancers = []
101+
enhancers = undefined
119102
} = options || {}
120103

121104
let rootReducer: Reducer<S, A>
@@ -142,7 +125,13 @@ export function configureStore<S = any, A extends Action = AnyAction>(
142125
})
143126
}
144127

145-
const storeEnhancers = [middlewareEnhancer, ...enhancers]
128+
let storeEnhancers: StoreEnhancer[] = [middlewareEnhancer]
129+
130+
if (Array.isArray(enhancers)) {
131+
storeEnhancers = [middlewareEnhancer, ...enhancers]
132+
} else if (typeof enhancers === 'function') {
133+
storeEnhancers = enhancers(storeEnhancers)
134+
}
146135

147136
const composedEnhancer = finalCompose(...storeEnhancers) as StoreEnhancer
148137

0 commit comments

Comments
 (0)