Repository: reduxjs/redux-toolkit
Pull request #549 (2020-05-10 20:55:14)

add middlewareBuilder notation with type-curried arguments to co…
This adds the option to pass a function instead of the middleware array as `middleware` option on `configureStore`.

This function receives a `middlewareBuilderApi` argument of the shape
```ts
type MiddlewareBuilderApi<S> = {
  defaultMiddleware: ThunkMiddlewareFor<S>[]
  getDefaultMiddleware: CurriedGetDefaultMiddleware<S>
}
```

Essentially that state-type-currying allows to do something like 
```ts
const store = configureStore({
      reducer: reducerA,
      middleware: ({ getDefaultMiddleware }) =>
        [
          otherMiddleware,
          ...getDefaultMiddleware({ thunk: false })
        ] as const
    })
```
which would before have been
```ts
const store = configureStore({
      reducer: reducerA,
      middleware: [
          otherMiddleware,
          ...getDefaultMiddleware<RootState, { thunk: false }>({ thunk: false })
        ] as const
    })
```

Also, it is now not necessary to know the `RootState` type, which makes the use of custom middlewares possible in combination with the object-type `reducer` notation in TS.


(Also, while touching those files anyway, I moved the separated "3.4 tests" back into normal test suite, as we're not supporting pre-3.4 TS any more)

netlify[bot]: Deploy preview for *redux-starter-kit-docs* ready!

Built with commit cf8e90f12f79be004d52abad3ee6f70bc20d3888

https://deploy-preview-549--redux-starter-kit-docs.netlify.app
codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/reduxjs/redux-toolkit/pr/549/builds/25692) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 763ba467edcc41a145f305458d7954c867c2cc11:

|Sandbox| Source |
|--|--|
|[modest-franklin-z553c](https://codesandbox.io/s/modest-franklin-z553c)| Configuration |
|[trusting-brook-yzdr2](https://codesandbox.io/s/trusting-brook-yzdr2)| Configuration |
|[modest-architecture-1elcn](https://codesandbox.io/s/modest-architecture-1elcn)| Configuration |

markerikson: What's the reasoning for passing in _both_ `getDefaultMiddleware` and `middleware`?
phryneas: I figured it might be the most common use case to just use the default middleware and extend it like

`middleware: ({defaultMiddleware}) => [ ...defaultMiddleware, logger ]`, but that some people also might want to change the options, so I added both.

But we can go the "only `getDefaultMiddleware`" route as well.
markerikson: Yeah, I get the thought of just handing them the array, but let's assume that if they're providing a callback here that they're probably going to want to customize something.  don't think it's too much work to have them call it here.
phryneas: Okay, I cut it down to just the `curriedGetDefaultMiddleware`.

Patch:
diff --git a/docs/api/createAsyncThunk.md b/docs/api/createAsyncThunk.md
index 42e72243..0a25097d 100644
--- a/docs/api/createAsyncThunk.md
+++ b/docs/api/createAsyncThunk.md
@@ -66,7 +66,7 @@ For example, a `type` argument of `'users/requestStatus'` will generate these ac
 
 ### `payloadCreator`
 
-A callback function that should return a promise containing the result of some asynchronous logic. It may also return a value synchronously. If there is an error, it should either return a rejected promise containing an `Error` instance or a plain value such as a descriptive error message or otherwise a resolved promise with a `RejectWithValue` argument as returned by the `thunkApi.rejectWithValue` function.
+A callback function that should return a promise containing the result of some asynchronous logic. It may also return a value synchronously. If there is an error, it should either return a rejected promise containing an `Error` instance or a plain value such as a descriptive error message or otherwise a resolved promise with a `RejectWithValue` argument as returned by the `thunkAPI.rejectWithValue` function.
 
 The `payloadCreator` function can contain whatever logic you need to calculate an appropriate result. This could include a standard AJAX data fetch request, multiple AJAX calls with the results combined into a final value, interactions with React Native `AsyncStorage`, and so on.
 
@@ -295,7 +295,7 @@ function MyComponent(props: { userId: string }) {
 
 After a thunk has been cancelled this way, it will dispatch (and return) a `"thunkName/rejected"` action with an `AbortError` on the `error` property. The thunk will not dispatch any further actions.
 
-Additionally, your `payloadCreator` can use the `AbortSignal` it is passed via `thunkApi.signal` to actually cancel a costly asynchronous action.
+Additionally, your `payloadCreator` can use the `AbortSignal` it is passed via `thunkAPI.signal` to actually cancel a costly asynchronous action.
 
 The `fetch` api of modern browsers already comes with support for an `AbortSignal`:
 
diff --git a/docs/introduction/quick-start.md b/docs/introduction/quick-start.md
index b4c042e4..1d54ef1e 100644
--- a/docs/introduction/quick-start.md
+++ b/docs/introduction/quick-start.md
@@ -31,7 +31,7 @@ Redux Toolkit includes these APIs:
 - [`createReducer()`](../api/createReducer.md): that lets you supply a lookup table of action types to case reducer functions, rather than writing switch statements. In addition, it automatically uses the [`immer` library](https://github.com/mweststrate/immer) to let you write simpler immutable updates with normal mutative code, like `state.todos[3].completed = true`.
 - [`createAction()`](../api/createAction.md): generates an action creator function for the given action type string. The function itself has `toString()` defined, so that it can be used in place of the type constant.
 - [`createSlice()`](../api/createSlice.md): accepts an object of reducer functions, a slice name, and an initial state value, and automatically generates a slice reducer with corresponding action creators and action types.
-- [`createAsyncThunk`](../api/createAsyncThunk.md): accepts an action type string and a function that returns a promise, and generates a thunk that dispatches `pending/resolved/rejected` action types based on that promise
+- [`createAsyncThunk`](../api/createAsyncThunk.md): accepts an action type string and a function that returns a promise, and generates a thunk that dispatches `pending/fulfilled/rejected` action types based on that promise
 - [`createEntityAdapter`](../api/createEntityAdapter.md): generates a set of reusable reducers and selectors to manage normalized data in the store
 - The [`createSelector` utility](../api/createSelector.md) from the [Reselect](https://github.com/reduxjs/reselect) library, re-exported for ease of use.
 
diff --git a/docs/tutorials/basic-tutorial.md b/docs/tutorials/basic-tutorial.md
index 0126470e..cb6c734a 100644
--- a/docs/tutorials/basic-tutorial.md
+++ b/docs/tutorials/basic-tutorial.md
@@ -157,7 +157,9 @@ function counter(state = 0, action) {
   }
 }
 
-const store = Redux.createStore(counter)
+const store = configureStore({
+  reducer: counter
+})
 
 document.getElementById('increment').addEventListener('click', () => {
   store.dispatch(increment())
diff --git a/docs/usage/usage-with-typescript.md b/docs/usage/usage-with-typescript.md
index ab1a5d46..c076b871 100644
--- a/docs/usage/usage-with-typescript.md
+++ b/docs/usage/usage-with-typescript.md
@@ -55,7 +55,7 @@ const store = configureStore({
 })
 
 export type AppDispatch = typeof store.dispatch
-export const useAppDispatch = () => useDispatch<AppDispatch>() // Export a hook that can be resused to resolve types
+export const useAppDispatch = () => useDispatch<AppDispatch>() // Export a hook that can be reused to resolve types
 ```
 
 ### Correct typings for the `Dispatch` type
diff --git a/etc/redux-toolkit.api.md b/etc/redux-toolkit.api.md
index 401db656..9a776fe4 100644
--- a/etc/redux-toolkit.api.md
+++ b/etc/redux-toolkit.api.md
@@ -112,7 +112,7 @@ export function configureStore<S = any, A extends Action = AnyAction, M extends
 export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction, M extends Middlewares<S> = Middlewares<S>> {
     devTools?: boolean | EnhancerOptions;
     enhancers?: StoreEnhancer[] | ConfigureEnhancersCallback;
-    middleware?: M;
+    middleware?: ((getDefaultMiddleware: CurriedGetDefaultMiddleware<S>) => M) | M;
     preloadedState?: DeepPartial<S extends any ? S : S>;
     reducer: Reducer<S, A> | ReducersMapObject<S, A>;
 }
diff --git a/src/configureStore.ts b/src/configureStore.ts
index 907641be..14637f67 100644
--- a/src/configureStore.ts
+++ b/src/configureStore.ts
@@ -20,8 +20,9 @@ import {
 
 import isPlainObject from './isPlainObject'
 import {
-  getDefaultMiddleware,
-  ThunkMiddlewareFor
+  ThunkMiddlewareFor,
+  curryGetDefaultMiddleware,
+  CurriedGetDefaultMiddleware
 } from './getDefaultMiddleware'
 import { DispatchForMiddlewares } from './tsHelpers'
 
@@ -56,7 +57,7 @@ export interface ConfigureStoreOptions<
    * An array of Redux middleware to install. If not supplied, defaults to
    * the set of middleware returned by `getDefaultMiddleware()`.
    */
-  middleware?: M
+  middleware?: ((getDefaultMiddleware: CurriedGetDefaultMiddleware<S>) => M) | M
 
   /**
    * Whether to enable Redux DevTools integration. Defaults to `true`.
@@ -124,9 +125,11 @@ export function configureStore<
   A extends Action = AnyAction,
   M extends Middlewares<S> = [ThunkMiddlewareFor<S>]
 >(options: ConfigureStoreOptions<S, A, M>): EnhancedStore<S, A, M> {
+  const curriedGetDefaultMiddleware = curryGetDefaultMiddleware<S>()
+
   const {
     reducer = undefined,
-    middleware = getDefaultMiddleware(),
+    middleware = curriedGetDefaultMiddleware(),
     devTools = true,
     preloadedState = undefined,
     enhancers = undefined
@@ -144,7 +147,11 @@ export function configureStore<
     )
   }
 
-  const middlewareEnhancer = applyMiddleware(...middleware)
+  const middlewareEnhancer = applyMiddleware(
+    ...(typeof middleware === 'function'
+      ? middleware(curriedGetDefaultMiddleware)
+      : middleware)
+  )
 
   let finalCompose = compose
 
diff --git a/src/createAsyncThunk.ts b/src/createAsyncThunk.ts
index 8e6b3e2d..d217dff4 100644
--- a/src/createAsyncThunk.ts
+++ b/src/createAsyncThunk.ts
@@ -199,12 +199,12 @@ interface AsyncThunkOptions<
    * A method to control whether the asyncThunk should be executed. Has access to the
    * `arg`, `api.getState()` and `api.extra` arguments.
    *
-   * @returns `true` if the asyncThunk should be executed, `false` if it should be skipped
+   * @returns `false` if it should be skipped
    */
   condition?(
     arg: ThunkArg,
     api: Pick<GetThunkAPI<ThunkApiConfig>, 'getState' | 'extra'>
-  ): boolean
+  ): boolean | undefined
   /**
    * If `condition` returns `false`, the asyncThunk will be skipped.
    * This option allows you to control whether a `rejected` action with `meta.condition == false`
diff --git a/src/getDefaultMiddleware.ts b/src/getDefaultMiddleware.ts
index 5efcf717..e3020252 100644
--- a/src/getDefaultMiddleware.ts
+++ b/src/getDefaultMiddleware.ts
@@ -39,6 +39,24 @@ export type ThunkMiddlewareFor<
       | ThunkMiddleware<S, AnyAction, null> //The ThunkMiddleware with a `null` ExtraArgument is here to provide backwards-compatibility.
       | ThunkMiddleware<S, AnyAction>
 
+export type CurriedGetDefaultMiddleware<S = any> = <
+  O extends Partial<GetDefaultMiddlewareOptions> = {
+    thunk: true
+    immutableCheck: true
+    serializableCheck: true
+  }
+>(
+  options?: O
+) => Array<Middleware<{}, S> | ThunkMiddlewareFor<S, O>>
+
+export function curryGetDefaultMiddleware<
+  S = any
+>(): CurriedGetDefaultMiddleware<S> {
+  return function curriedGetDefaultMiddleware(options) {
+    return getDefaultMiddleware(options)
+  }
+}
+
 /**
  * Returns any array containing the default middleware installed by
  * `configureStore()`. Useful if you want to configure your store with a custom
diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js
index a25cf4a6..c0ee0de1 100644
--- a/website/docusaurus.config.js
+++ b/website/docusaurus.config.js
@@ -84,7 +84,7 @@ module.exports = {
           ]
         }
       ],
-      copyright: `Copyright (c) 2015-present Dan Abramov and the Redux documentation authors.`
+      copyright: `Copyright © 2015–${new Date().getFullYear()} Dan Abramov and the Redux documentation authors.`
     },
     image: 'img/redux-logo-landscape.png',
     navbar: {


Test patch:
diff --git a/src/configureStore.test.ts b/src/configureStore.test.ts
index a7a4bab6..b8f1f726 100644
--- a/src/configureStore.test.ts
+++ b/src/configureStore.test.ts
@@ -84,6 +84,26 @@ describe('configureStore', () => {
     })
   })
 
+  describe('middleware builder notation', () => {
+    it('calls builder, passes getDefaultMiddleware and uses returned middlewares', () => {
+      const thank = jest.fn((_store => next => action =>
+        'foobar') as redux.Middleware)
+
+      const builder = jest.fn(getDefaultMiddleware => {
+        expect(getDefaultMiddleware).toEqual(expect.any(Function))
+        expect(getDefaultMiddleware()).toEqual(expect.any(Array))
+
+        return [thank]
+      })
+
+      const store = configureStore({ middleware: builder, reducer })
+
+      expect(builder).toHaveBeenCalled()
+
+      expect(store.dispatch({ type: 'test' })).toBe('foobar')
+    })
+  })
+
   describe('with devTools disabled', () => {
     it('calls createStore without devTools enhancer', () => {
       expect(configureStore({ devTools: false, reducer })).toBeInstanceOf(
diff --git a/type-tests/files/configureStore.typetest.ts b/type-tests/files/configureStore.typetest.ts
index 957b2432..93a6984f 100644
--- a/type-tests/files/configureStore.typetest.ts
+++ b/type-tests/files/configureStore.typetest.ts
@@ -270,4 +270,56 @@ import thunk, { ThunkMiddleware, ThunkAction, ThunkDispatch } from 'redux-thunk'
     // typings:expect-error
     store.dispatch(function() {} as ThunkAction<void, {}, boolean, AnyAction>)
   }
+
+  /**
+   * Test: custom middleware and getDefaultMiddleware
+   */
+  {
+    const store = configureStore({
+      reducer: reducerA,
+      middleware: [
+        ((() => {}) as any) as Middleware<(a: 'a') => 'A', StateA>,
+        ...getDefaultMiddleware<StateA>()
+      ] as const
+    })
+    const result1: 'A' = store.dispatch('a')
+    const result2: Promise<'A'> = store.dispatch(thunkA())
+    // typings:expect-error
+    store.dispatch(thunkB())
+  }
+
+  /**
+   * Test: middlewareBuilder notation, getDefaultMiddleware (unconfigured)
+   */
+  {
+    const store = configureStore({
+      reducer: reducerA,
+      middleware: getDefaultMiddleware =>
+        [
+          ((() => {}) as any) as Middleware<(a: 'a') => 'A', StateA>,
+          ...getDefaultMiddleware()
+        ] as const
+    })
+    const result1: 'A' = store.dispatch('a')
+    const result2: Promise<'A'> = store.dispatch(thunkA())
+    // typings:expect-error
+    store.dispatch(thunkB())
+  }
+
+  /**
+   * Test: middlewareBuilder notation, getDefaultMiddleware (thunk: false)
+   */
+  {
+    const store = configureStore({
+      reducer: reducerA,
+      middleware: getDefaultMiddleware =>
+        [
+          ((() => {}) as any) as Middleware<(a: 'a') => 'A', StateA>,
+          ...getDefaultMiddleware({ thunk: false })
+        ] as const
+    })
+    const result1: 'A' = store.dispatch('a')
+    // typings:expect-error
+    store.dispatch(thunkA())
+  }
 }
diff --git a/type-tests/files/min-3.4/configureStore.typetest.ts b/type-tests/files/min-3.4/configureStore.typetest.ts
deleted file mode 100644
index 4a739947..00000000
--- a/type-tests/files/min-3.4/configureStore.typetest.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { Dispatch, Middleware } from 'redux'
-import { configureStore, getDefaultMiddleware } from 'src'
-import { ThunkAction } from 'redux-thunk'
-
-/**
- * Test: Dispatch typings
- */
-{
-  type StateA = number
-  const reducerA = () => 0
-  function thunkA() {
-    return ((() => {}) as any) as ThunkAction<Promise<'A'>, StateA, any, any>
-  }
-
-  type StateB = string
-  function thunkB() {
-    return (dispatch: Dispatch, getState: () => StateB) => {}
-  }
-
-  /**
-   * Test: custom middleware and getDefaultMiddleware
-   */
-  {
-    const store = configureStore({
-      reducer: reducerA,
-      middleware: [
-        ((() => {}) as any) as Middleware<(a: 'a') => 'A', StateA>,
-        ...getDefaultMiddleware<StateA>()
-      ] as const
-    })
-    const result1: 'A' = store.dispatch('a')
-    const result2: Promise<'A'> = store.dispatch(thunkA())
-    // typings:expect-error
-    store.dispatch(thunkB())
-  }
-}
diff --git a/type-tests/types-min-3.4.test.ts b/type-tests/types-min-3.4.test.ts
deleted file mode 100644
index b4c04c6b..00000000
--- a/type-tests/types-min-3.4.test.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { testIf, checkDirectory, tsVersion } from './lib'
-
-testIf(tsVersion >= 3.4)('Types >= 3.4', () => {
-  checkDirectory(`${__dirname}/files/min-3.4`)
-})