Skip to content

Adding correctly typed ` prepend and ` concat to the array… #559

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

Merged
merged 9 commits into from
Jun 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion docs/api/getDefaultMiddleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,29 @@ middleware added as well:
```js
const store = configureStore({
reducer: rootReducer,
middleware: [...getDefaultMiddleware(), logger]
middleware: getDefaultMiddleware().concat(logger)
})

// Store has all of the default middleware added, _plus_ the logger middleware
```

## Usage without import (middleware callback notation)

For convenience, the `middleware` property of `configureStore` can be used with a callback notation like this.

```js
const store = configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(logger)
})

// Store has all of the default middleware added, _plus_ the logger middleware
```

While this does not make much of a difference for JavaScript users, using this notation is preferrable to TypeScript users, as that version of `getDefaultMiddleware` is already correctly pre-typed for the `Store`'s type, so no use of generics is necessary.

Also, when using TypeScript, it is preferrable to use the chainable `.concat(...)` and `.prepend(...)` methods of the returned `MiddlewareArray` instead of the array spread operator, as the latter can lose valuable type information under some circumstances.

## Included Default Middleware

### Development
Expand Down
51 changes: 37 additions & 14 deletions docs/usage/usage-with-typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,37 +62,60 @@ export const useAppDispatch = () => useDispatch<AppDispatch>() // Export a hook

The type of the `dispatch` function type will be directly inferred from the `middleware` option. So if you add _correctly typed_ middlewares, `dispatch` should already be correctly typed.

There might however be cases, where TypeScript decides to simplify your provided middleware array down to just `Array<Middleware>`. In that case, you have to either specify the array type manually as a tuple, or in TS versions >= 3.4, just add `as const` to your definition.
As TypeScript often widens array types when combining arrays using the spread operator, we suggest using the `.concat(...)` and `.prepend(...)` methods of the `MiddlewareArray` returned by `getDefaultMiddleware()`.

Please note that when calling `getDefaultMiddleware` in TypeScript, you have to provide the state type as a generic argument.
Also, we suggest using the callback notation for the `middleware` option go get a correctly pre-typed version of `getDefaultMiddleware` that does not require you to specify any generics by hand.

```ts {10-20}
import { configureStore } from '@reduxjs/toolkit'
import additionalMiddleware from 'additional-middleware'
import logger from 'redux-logger'
// @ts-ignore
import untypedMiddleware from 'untyped-middleware'
import rootReducer from './rootReducer'

type RootState = ReturnType<typeof rootReducer>
const store = configureStore({
reducer: rootReducer,
middleware: [
// getDefaultMiddleware needs to be called with the state type
...getDefaultMiddleware<RootState>(),
// correctly typed middlewares can just be used
additionalMiddleware,
// you can also manually type middlewares manually
untypedMiddleware as Middleware<
(action: Action<'specialAction'>) => number,
RootState
>
] as const // prevent this from becoming just `Array<Middleware>`
middleware: getDefaultMiddleware =>
getDefaultMiddleware()
.prepend(
// correctly typed middlewares can just be used
additionalMiddleware,
// you can also manually type middlewares manually
untypedMiddleware as Middleware<
(action: Action<'specialAction'>) => number,
RootState
>
)
// prepend and concat calls can be chained
.concat(logger)
})

type AppDispatch = typeof store.dispatch
```

If you need any additional reference or examples, [the type tests for `configureStore`](https://github.com/reduxjs/redux-toolkit/blob/master/type-tests/files/configureStore.typetest.ts) contain many different scenarios on how to type this.
#### Using `MiddlewareArray` without `getDefaultMiddleware`

If you want to skip the usage of `getDefaultMiddleware` altogether, you can still use `MiddlewareArray` for type-safe concatenation of your `middleware` array. This class extends the default JavaScript `Array` type, only with modified typings for `.concat(...)` and the additional `.prepend(...)` method.

This is generally not required though, as you will probably not run into any array-type-widening issues as long as you are using `as const` and do not use the spread operator.

So the following two calls would be equivalent:

```ts
import { configureStore, MiddlewareArray } from '@reduxjs/toolkit'

configureStore({
reducer: rootReducer,
middleware: new MiddlewareArray().concat(additionalMiddleware, logger)
})

configureStore({
reducer: rootReducer,
middleware: [additionalMiddleware, logger] as const
})
```

### Using the extracted `Dispatch` type with React-Redux

Expand Down
16 changes: 14 additions & 2 deletions etc/redux-toolkit.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
}
Expand Down Expand Up @@ -279,7 +279,7 @@ export function getDefaultMiddleware<S = any, O extends Partial<GetDefaultMiddle
thunk: true;
immutableCheck: true;
serializableCheck: true;
}>(options?: O): Array<Middleware<{}, S> | ThunkMiddlewareFor<S, O>>;
}>(options?: O): MiddlewareArray<Middleware<{}, S> | ThunkMiddlewareFor<S, O>>;

// @public
export function getType<T extends string>(actionCreator: PayloadActionCreator<any, T>): T;
Expand All @@ -305,6 +305,18 @@ export function isImmutableDefault(value: unknown): boolean;
// @public
export function isPlain(val: any): boolean;

// @public (undocumented)
export class MiddlewareArray<Middlewares extends Middleware<any, any>> extends Array<Middlewares> {
// (undocumented)
concat<AdditionalMiddlewares extends ReadonlyArray<Middleware<any, any>>>(items: AdditionalMiddlewares): MiddlewareArray<Middlewares | AdditionalMiddlewares[number]>;
// (undocumented)
concat<AdditionalMiddlewares extends ReadonlyArray<Middleware<any, any>>>(...items: AdditionalMiddlewares): MiddlewareArray<Middlewares | AdditionalMiddlewares[number]>;
// (undocumented)
prepend<AdditionalMiddlewares extends ReadonlyArray<Middleware<any, any>>>(items: AdditionalMiddlewares): MiddlewareArray<AdditionalMiddlewares[number] | Middlewares>;
// (undocumented)
prepend<AdditionalMiddlewares extends ReadonlyArray<Middleware<any, any>>>(...items: AdditionalMiddlewares): MiddlewareArray<AdditionalMiddlewares[number] | Middlewares>;
}

// @public (undocumented)
export let nanoid: (size?: number) => string;

Expand Down
20 changes: 20 additions & 0 deletions src/configureStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
17 changes: 12 additions & 5 deletions src/configureStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import {

import isPlainObject from './isPlainObject'
import {
getDefaultMiddleware,
ThunkMiddlewareFor
ThunkMiddlewareFor,
curryGetDefaultMiddleware,
CurriedGetDefaultMiddleware
} from './getDefaultMiddleware'
import { DispatchForMiddlewares } from './tsHelpers'

Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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
Expand All @@ -144,7 +147,11 @@ export function configureStore<
)
}

const middlewareEnhancer = applyMiddleware(...middleware)
const middlewareEnhancer = applyMiddleware(
...(typeof middleware === 'function'
? middleware(curriedGetDefaultMiddleware)
: middleware)
)

let finalCompose = compose

Expand Down
1 change: 1 addition & 0 deletions src/createAsyncThunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ If you want to use the AbortController to react to \`abort\` events, please cons
options.condition &&
options.condition(arg, { getState, extra }) === false
) {
// eslint-disable-next-line no-throw-literal
throw {
name: 'ConditionError',
message: 'Aborted due to condition callback returning false.'
Expand Down
122 changes: 119 additions & 3 deletions src/getDefaultMiddleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { AnyAction } from 'redux'
import { getDefaultMiddleware } from './getDefaultMiddleware'
import { configureStore } from './configureStore'
import { AnyAction, Middleware } from 'redux'
import { getDefaultMiddleware, MiddlewareArray, configureStore } from '.'
import thunk, { ThunkAction } from 'redux-thunk'

describe('getDefaultMiddleware', () => {
Expand Down Expand Up @@ -116,3 +115,120 @@ describe('getDefaultMiddleware', () => {
expect(serializableCheckWasCalled).toBe(true)
})
})

describe('MiddlewareArray functionality', () => {
const middleware1: Middleware = () => next => action => next(action)
const middleware2: Middleware = () => next => action => next(action)
const defaultMiddleware = getDefaultMiddleware()
const originalDefaultMiddleware = [...defaultMiddleware]

test('allows to prepend a single value', () => {
const prepended = defaultMiddleware.prepend(middleware1)

// value is prepended
expect(prepended).toEqual([middleware1, ...defaultMiddleware])
// returned value is of correct type
expect(prepended).toBeInstanceOf(MiddlewareArray)
// prepended is a new array
expect(prepended).not.toEqual(defaultMiddleware)
// defaultMiddleware is not modified
expect(defaultMiddleware).toEqual(originalDefaultMiddleware)
})

test('allows to prepend multiple values (array as first argument)', () => {
const prepended = defaultMiddleware.prepend([middleware1, middleware2])

// value is prepended
expect(prepended).toEqual([middleware1, middleware2, ...defaultMiddleware])
// returned value is of correct type
expect(prepended).toBeInstanceOf(MiddlewareArray)
// prepended is a new array
expect(prepended).not.toEqual(defaultMiddleware)
// defaultMiddleware is not modified
expect(defaultMiddleware).toEqual(originalDefaultMiddleware)
})

test('allows to prepend multiple values (rest)', () => {
const prepended = defaultMiddleware.prepend(middleware1, middleware2)

// value is prepended
expect(prepended).toEqual([middleware1, middleware2, ...defaultMiddleware])
// returned value is of correct type
expect(prepended).toBeInstanceOf(MiddlewareArray)
// prepended is a new array
expect(prepended).not.toEqual(defaultMiddleware)
// defaultMiddleware is not modified
expect(defaultMiddleware).toEqual(originalDefaultMiddleware)
})

test('allows to concat a single value', () => {
const concatenated = defaultMiddleware.concat(middleware1)

// value is concatenated
expect(concatenated).toEqual([...defaultMiddleware, middleware1])
// returned value is of correct type
expect(concatenated).toBeInstanceOf(MiddlewareArray)
// concatenated is a new array
expect(concatenated).not.toEqual(defaultMiddleware)
// defaultMiddleware is not modified
expect(defaultMiddleware).toEqual(originalDefaultMiddleware)
})

test('allows to concat multiple values (array as first argument)', () => {
const concatenated = defaultMiddleware.concat([middleware1, middleware2])

// value is concatenated
expect(concatenated).toEqual([
...defaultMiddleware,
middleware1,
middleware2
])
// returned value is of correct type
expect(concatenated).toBeInstanceOf(MiddlewareArray)
// concatenated is a new array
expect(concatenated).not.toEqual(defaultMiddleware)
// defaultMiddleware is not modified
expect(defaultMiddleware).toEqual(originalDefaultMiddleware)
})

test('allows to concat multiple values (rest)', () => {
const concatenated = defaultMiddleware.concat(middleware1, middleware2)

// value is concatenated
expect(concatenated).toEqual([
...defaultMiddleware,
middleware1,
middleware2
])
// returned value is of correct type
expect(concatenated).toBeInstanceOf(MiddlewareArray)
// concatenated is a new array
expect(concatenated).not.toEqual(defaultMiddleware)
// defaultMiddleware is not modified
expect(defaultMiddleware).toEqual(originalDefaultMiddleware)
})

test('allows to concat and then prepend', () => {
const concatenated = defaultMiddleware
.concat(middleware1)
.prepend(middleware2)

expect(concatenated).toEqual([
middleware2,
...defaultMiddleware,
middleware1
])
})

test('allows to prepend and then concat', () => {
const concatenated = defaultMiddleware
.prepend(middleware2)
.concat(middleware1)

expect(concatenated).toEqual([
middleware2,
...defaultMiddleware,
middleware1
])
})
})
Loading