Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable customizing default middleware and store enhancers #192

Merged
merged 11 commits into from
Sep 8, 2019
80 changes: 63 additions & 17 deletions docs/api/configureStore.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,62 @@ hide_title: true

# `configureStore`

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

## Parameters

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

```ts
function configureStore({
// A single reducer function that will be used as the root reducer,
// or an object of slice reducers that will be passed to combineReducers()
reducer: Object<string, ReducerFunction> | ReducerFunction,
// An array of Redux middlewares. If not supplied, uses getDefaultMiddleware()
middleware?: MiddlewareFunction[],
// Enable support for the Redux DevTools Extension. Defaults to true.
devTools?: boolean | EnhancerOptions,
// Same as current createStore.
preloadedState?: State,
// An optional array of Redux store enhancers
enhancers?: ReduxStoreEnhancer[],
})
type ConfigureEnhancersCallback = (
defaultEnhancers: StoreEnhancer[]
) => StoreEnhancer[]

interface ConfigureStoreOptions<S = any, A extends Action = AnyAction> {
/**
* A single reducer function that will be used as the root reducer, or an
* object of slice reducers that will be passed to `combineReducers()`.
*/
reducer: Reducer<S, A> | ReducersMapObject<S, A>

/**
* An array of Redux middleware to install. If not supplied, defaults to
* the set of middleware returned by `getDefaultMiddleware()`.
*/
middleware?: Middleware<{}, S>[]

/**
* Whether to enable Redux DevTools integration. Defaults to `true`.
*
* Additional configuration can be done by passing Redux DevTools options
*/
devTools?: boolean | DevToolsOptions

/**
* The initial state, same as Redux's createStore.
* You may optionally specify it to hydrate the state
* from the server in universal apps, or to restore a previously serialized
* user session. If you use `combineReducers()` to produce the root reducer
* function (either directly or indirectly by passing an object as `reducer`),
* this must be an object with the same shape as the reducer map keys.
*/
preloadedState?: DeepPartial<S extends any ? S : S>

/**
* The store enhancers to apply. See Redux's `createStore()`.
* All enhancers will be included before the DevTools Extension enhancer.
* If you need to customize the order of enhancers, supply a callback
* function that will receive the original array (ie, `[applyMiddleware]`),
* and should return a new array (such as `[applyMiddleware, offline]`).
* If you only need to add middleware, use the `middleware` parameter instead.
*/
enhancers?: StoreEnhancer[] | ConfigureEnhancersCallback
}

function configureStore<S = any, A extends Action = AnyAction>(
options: ConfigureStoreOptions<S, A>
): EnhancedStore<S, A>
```

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

### `enhancers`

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`.
An optional array of Redux store enhancers, or a callback function to customize the array of enhancers.

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

This should _not_ include `applyMiddleware()` or the Redux DevTools Extension `composeWithDevTools`, as those are already handled by `configureStore`.

Example: `enhancers: [offline]` will result in a final setup of `[applyMiddleware, offline, devToolsExtension]`.

If defined as a callback function, it will be called with the existing array of enhancers _without_ the DevTools Extension (currently `[applyMiddleware]`),
and should return a new array of enhancers. This is primarily useful for cases where a store enhancer needs to be added
in front of `applyMiddleware`, such as `redux-first-router` or `redux-offline`.

This should _not_ include `applyMiddleware()` or
the Redux DevTools Extension `composeWithDevTools`, as those are already handled by `configureStore`.
Example: `enhancers: (defaultEnhancers) => [offline, ...defaultEnhancers]` will result in a final setup
of `[offline, applyMiddleware, devToolsExtension]`.

## Usage

Expand Down
66 changes: 64 additions & 2 deletions docs/api/getDefaultMiddleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,75 @@ by default, since thunks are the basic recommended side effects middleware for R
Currently, the return value is:

```js
;[immutableStateInvariant, thunk, serializableStateInvariant]
const middleware = [thunk, immutableStateInvariant, serializableStateInvariant]
```

### Production

Currently, the return value is:

```js
;[thunk]
const middleware = [thunk]
```

## Customizing the Included Middleware

`getDefaultMiddleware` accepts an options object that allows customizing each middleware in two ways:

- Each middleware can be excluded from inclusion in the array by passing `false` for its corresponding field
- Each middleware can have its options customized by passing the matching options object for its corresponding field

This example shows excluding the serializable state check middleware, and passing a specific value for the thunk
middleware's "extra argument":

```ts
const customizedMiddleware = getDefaultMiddleware({
thunk: {
extraArgument: myCustomApiService
},
serializableCheck: false
})
```

## API Reference

```ts
interface ThunkOptions<E = any> {
extraArgument: E
}

interface ImmutableStateInvariantMiddlewareOptions {
isImmutable?: (value: any) => boolean
ignore?: string[]
}

interface SerializableStateInvariantMiddlewareOptions {
/**
* The function to check if a value is considered serializable. This
* function is applied recursively to every value contained in the
* state. Defaults to `isPlain()`.
*/
isSerializable?: (value: any) => boolean
/**
* The function that will be used to retrieve entries from each
* value. If unspecified, `Object.entries` will be used. Defaults
* to `undefined`.
*/
getEntries?: (value: any) => [string, any][]

/**
* An array of action types to ignore when checking for serializability, Defaults to []
*/
ignoredActions?: string[]
}

interface GetDefaultMiddlewareOptions {
thunk?: boolean | ThunkOptions
immutableCheck?: boolean | ImmutableStateInvariantMiddlewareOptions
serializableCheck?: boolean | SerializableStateInvariantMiddlewareOptions
}

function getDefaultMiddleware<S = any>(
options: GetDefaultMiddlewareOptions = {}
): Middleware<{}, S>[]
```
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"@babel/preset-typescript": "^7.3.3",
"@types/jest": "^24.0.11",
"@types/node": "^10.14.4",
"@types/redux-immutable-state-invariant": "^2.1.0",
"@types/redux-immutable-state-invariant": "^2.1.1",
"@typescript-eslint/parser": "^1.6.0",
"babel-eslint": "^10.0.1",
"eslint": "^5.16.0",
Expand Down
54 changes: 31 additions & 23 deletions src/configureStore.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,13 @@
import { configureStore, getDefaultMiddleware } from './configureStore'
import { configureStore } from './configureStore'
import * as redux from 'redux'
import * as devtools from 'redux-devtools-extension'

import thunk from 'redux-thunk'

describe('getDefaultMiddleware', () => {
const ORIGINAL_NODE_ENV = process.env.NODE_ENV

afterEach(() => {
process.env.NODE_ENV = ORIGINAL_NODE_ENV
})

it('returns an array with only redux-thunk in production', () => {
process.env.NODE_ENV = 'production'

expect(getDefaultMiddleware()).toEqual([thunk])
})

it('returns an array with additional middleware in development', () => {
const middleware = getDefaultMiddleware()
expect(middleware).toContain(thunk)
expect(middleware.length).toBeGreaterThan(1)
})
})
import {
StoreCreator,
StoreEnhancer,
StoreEnhancerStoreCreator,
Reducer,
AnyAction
} from 'redux'

describe('configureStore', () => {
jest.spyOn(redux, 'applyMiddleware')
Expand Down Expand Up @@ -166,5 +151,28 @@ describe('configureStore', () => {
expect.any(Function)
)
})

it('accepts a callback for customizing enhancers', () => {
let dummyEnhancerCalled = false

const dummyEnhancer: StoreEnhancer = (
createStore: StoreEnhancerStoreCreator
) => (reducer, ...args: any[]) => {
dummyEnhancerCalled = true

return createStore(reducer, ...args)
}

const reducer = () => ({})

const store = configureStore({
reducer,
enhancers: defaultEnhancers => {
return [...defaultEnhancers, dummyEnhancer]
}
})

expect(dummyEnhancerCalled).toBe(true)
})
})
})
67 changes: 28 additions & 39 deletions src/configureStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,20 @@ import {
Store,
DeepPartial
} from 'redux'
import { composeWithDevTools, EnhancerOptions } from 'redux-devtools-extension'
import thunk, { ThunkDispatch, ThunkMiddleware } from 'redux-thunk'

// UMD-DEV-ONLY: import createImmutableStateInvariantMiddleware from 'redux-immutable-state-invariant'

import { createSerializableStateInvariantMiddleware } from './serializableStateInvariantMiddleware'
import {
composeWithDevTools,
EnhancerOptions as DevToolsOptions
} from 'redux-devtools-extension'
import { ThunkDispatch } from 'redux-thunk'

import isPlainObject from './isPlainObject'
import { getDefaultMiddleware } from './getDefaultMiddleware'

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

/**
* Returns any array containing the default middleware installed by
* `configureStore()`. Useful if you want to configure your store with a custom
* `middleware` array but still keep the default set.
*
* @return The default middleware used by `configureStore()`.
*/
export function getDefaultMiddleware<S = any, A extends Action = AnyAction>(): [
ThunkMiddleware<S, A>,
...Middleware<{}, S>[]
] {
let middlewareArray: [ThunkMiddleware<S, A>, ...Middleware<{}, S>[]] = [thunk]

if (process.env.NODE_ENV !== 'production') {
/* START_REMOVE_UMD */
const createImmutableStateInvariantMiddleware = require('redux-immutable-state-invariant')
.default
middlewareArray.unshift(createImmutableStateInvariantMiddleware())
/* STOP_REMOVE_UMD */

middlewareArray.push(createSerializableStateInvariantMiddleware())
}

return middlewareArray
}
export type ConfigureEnhancersCallback = (
defaultEnhancers: StoreEnhancer[]
) => StoreEnhancer[]

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

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

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

/**
Expand All @@ -115,7 +98,7 @@ export function configureStore<S = any, A extends Action = AnyAction>(
middleware = getDefaultMiddleware(),
devTools = true,
preloadedState = undefined,
enhancers = []
enhancers = undefined
} = options || {}

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

const storeEnhancers = [middlewareEnhancer, ...enhancers]
let storeEnhancers: StoreEnhancer[] = [middlewareEnhancer]

if (Array.isArray(enhancers)) {
storeEnhancers = [middlewareEnhancer, ...enhancers]
} else if (typeof enhancers === 'function') {
storeEnhancers = enhancers(storeEnhancers)
}

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

Expand Down
Loading