Skip to content

Commit

Permalink
Merge pull request #3460 from reduxjs/special-array
Browse files Browse the repository at this point in the history
  • Loading branch information
markerikson authored May 29, 2023
2 parents 67b13b5 + 9fa849d commit a2f3c9a
Show file tree
Hide file tree
Showing 20 changed files with 269 additions and 202 deletions.
3 changes: 2 additions & 1 deletion docs/api/actionCreatorMiddleware.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export default function (state = {}, action: any) {
import {
configureStore,
createActionCreatorInvariantMiddleware,
Tuple,
} from '@reduxjs/toolkit'
import reducer from './reducer'

Expand All @@ -62,6 +63,6 @@ const actionCreatorMiddleware = createActionCreatorInvariantMiddleware({

const store = configureStore({
reducer,
middleware: [actionCreatorMiddleware],
middleware: new Tuple(actionCreatorMiddleware),
})
```
34 changes: 33 additions & 1 deletion docs/api/configureStore.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,22 @@ and should return a middleware array.
For more details on how the `middleware` parameter works and the list of middleware that are added by default, see the
[`getDefaultMiddleware` docs page](./getDefaultMiddleware.mdx).

:::note Tuple
Typescript users are required to use a `Tuple` instance (if not using a `getDefaultMiddleware` result, which is already a `Tuple`), for better inference.

```ts no-transpile
import { configureStore, Tuple } from '@reduxjs/toolkit'

configureStore({
reducer: rootReducer,
middleware: new Tuple(additionalMiddleware, logger),
})
```
Javascript-only users are free to use a plain array if preferred.
:::
### `devTools`
If this is a boolean, it will be used to indicate whether `configureStore` should automatically enable support for [the Redux DevTools browser extension](https://github.com/reduxjs/redux-devtools).
Expand Down Expand Up @@ -122,7 +138,7 @@ If defined as an array, these will be passed to [the Redux `compose` function](h
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]`.
Example: `enhancers: new Tuple(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
Expand All @@ -131,6 +147,22 @@ in front of `applyMiddleware`, such as `redux-first-router` or `redux-offline`.
Example: `enhancers: (defaultEnhancers) => defaultEnhancers.prepend(offline)` will result in a final setup
of `[offline, applyMiddleware, devToolsExtension]`.
:::note Tuple
Typescript users are required to use a `Tuple` instance (if not using a `getDefaultEnhancer` result, which is already a `Tuple`), for better inference.
```ts no-transpile
import { configureStore, Tuple } from '@reduxjs/toolkit'

configureStore({
reducer: rootReducer,
enhancers: new Tuple(offline),
})
```
Javascript-only users are free to use a plain array if preferred.
:::
## Usage
### Basic Example
Expand Down
4 changes: 2 additions & 2 deletions docs/api/getDefaultMiddleware.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ If you want to customize the list of middleware, you can supply an array of midd
```js
const store = configureStore({
reducer: rootReducer,
middleware: [thunk, logger],
middleware: new Tuple(thunk, logger),
})

// Store specifically has the thunk and logger middleware applied
Expand All @@ -55,7 +55,7 @@ const store = configureStore({
// Store has all of the default middleware added, _plus_ the logger middleware
```

It is preferable 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.
It is preferable to use the chainable `.concat(...)` and `.prepend(...)` methods of the returned `Tuple` instead of the array spread operator, as the latter can lose valuable type information under some circumstances.

## Included Default Middleware

Expand Down
3 changes: 2 additions & 1 deletion docs/api/immutabilityMiddleware.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export default exampleSlice.reducer
import {
configureStore,
createImmutableStateInvariantMiddleware,
Tuple,
} from '@reduxjs/toolkit'

import exampleSliceReducer from './exampleSlice'
Expand All @@ -85,7 +86,7 @@ const immutableInvariantMiddleware = createImmutableStateInvariantMiddleware({
const store = configureStore({
reducer: exampleSliceReducer,
// Note that this will replace all default middleware
middleware: [immutableInvariantMiddleware],
middleware: new Tuple(immutableInvariantMiddleware),
})
```

Expand Down
3 changes: 2 additions & 1 deletion docs/api/serializabilityMiddleware.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import {
configureStore,
createSerializableStateInvariantMiddleware,
isPlain,
Tuple,
} from '@reduxjs/toolkit'
import reducer from './reducer'

Expand All @@ -110,7 +111,7 @@ const serializableMiddleware = createSerializableStateInvariantMiddleware({

const store = configureStore({
reducer,
middleware: [serializableMiddleware],
middleware: new Tuple(serializableMiddleware),
})
```

Expand Down
19 changes: 6 additions & 13 deletions docs/usage/usage-with-typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export default store

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.

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()`.
As TypeScript often widens array types when combining arrays using the spread operator, we suggest using the `.concat(...)` and `.prepend(...)` methods of the `Tuple` returned by `getDefaultMiddleware()`.

```ts
import { configureStore } from '@reduxjs/toolkit'
Expand Down Expand Up @@ -134,25 +134,18 @@ export type AppDispatch = typeof store.dispatch
export default store
```

#### Using `MiddlewareArray` without `getDefaultMiddleware`
#### Using `Tuple` 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.
If you want to skip the usage of `getDefaultMiddleware` altogether, you are required to use `Tuple` for type-safe creation 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:
For example:

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

configureStore({
reducer: rootReducer,
middleware: new MiddlewareArray().concat(additionalMiddleware, logger),
})
import { configureStore, Tuple } from '@reduxjs/toolkit'

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

Expand Down
18 changes: 9 additions & 9 deletions packages/toolkit/src/configureStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import type {
ExtractStoreExtensions,
ExtractStateExtensions,
} from './tsHelpers'
import type { EnhancerArray, MiddlewareArray } from './utils'
import type { Tuple } from './utils'
import type { GetDefaultEnhancers } from './getDefaultEnhancers'
import { buildGetDefaultEnhancers } from './getDefaultEnhancers'

Expand All @@ -37,8 +37,8 @@ const IS_PRODUCTION = process.env.NODE_ENV === 'production'
export interface ConfigureStoreOptions<
S = any,
A extends Action = AnyAction,
M extends Middlewares<S> = Middlewares<S>,
E extends Enhancers = Enhancers,
M extends Tuple<Middlewares<S>> = Tuple<Middlewares<S>>,
E extends Tuple<Enhancers> = Tuple<Enhancers>,
P = S
> {
/**
Expand All @@ -48,8 +48,8 @@ export interface ConfigureStoreOptions<
reducer: Reducer<S, A, P> | ReducersMapObject<S, A, P>

/**
* An array of Redux middleware to install. If not supplied, defaults to
* the set of middleware returned by `getDefaultMiddleware()`.
* An array of Redux middleware to install, or a callback receiving `getDefaultMiddleware` and returning a Tuple of middleware.
* If not supplied, defaults to the set of middleware returned by `getDefaultMiddleware()`.
*
* @example `middleware: (gDM) => gDM().concat(logger, apiMiddleware, yourCustomMiddleware)`
* @see https://redux-toolkit.js.org/api/getDefaultMiddleware#intended-usage
Expand Down Expand Up @@ -78,8 +78,8 @@ export interface ConfigureStoreOptions<
* 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 a `getDefaultEnhancers` function that returns an EnhancerArray,
* and should return a new array (such as `getDefaultEnhancers().concat(offline)`).
* function that will receive a `getDefaultEnhancers` function that returns a Tuple,
* and should return a Tuple of enhancers (such as `getDefaultEnhancers().concat(offline)`).
* If you only need to add middleware, you can use the `middleware` parameter instead.
*/
enhancers?: ((getDefaultEnhancers: GetDefaultEnhancers<M>) => E) | E
Expand Down Expand Up @@ -112,8 +112,8 @@ export type EnhancedStore<
export function configureStore<
S = any,
A extends Action = AnyAction,
M extends Middlewares<S> = MiddlewareArray<[ThunkMiddlewareFor<S>]>,
E extends Enhancers = EnhancerArray<
M extends Tuple<Middlewares<S>> = Tuple<[ThunkMiddlewareFor<S>]>,
E extends Tuple<Enhancers> = Tuple<
[StoreEnhancer<{ dispatch: ExtractDispatchExtensions<M> }>, StoreEnhancer]
>,
P = S
Expand Down
6 changes: 3 additions & 3 deletions packages/toolkit/src/getDefaultEnhancers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { StoreEnhancer } from 'redux'
import type { AutoBatchOptions } from './autoBatchEnhancer'
import { autoBatchEnhancer } from './autoBatchEnhancer'
import { EnhancerArray } from './utils'
import { Tuple } from './utils'
import type { Middlewares } from './configureStore'
import type { ExtractDispatchExtensions } from './tsHelpers'

Expand All @@ -11,15 +11,15 @@ type GetDefaultEnhancersOptions = {

export type GetDefaultEnhancers<M extends Middlewares<any>> = (
options?: GetDefaultEnhancersOptions
) => EnhancerArray<[StoreEnhancer<{ dispatch: ExtractDispatchExtensions<M> }>]>
) => Tuple<[StoreEnhancer<{ dispatch: ExtractDispatchExtensions<M> }>]>

export const buildGetDefaultEnhancers = <M extends Middlewares<any>>(
middlewareEnhancer: StoreEnhancer<{ dispatch: ExtractDispatchExtensions<M> }>
): GetDefaultEnhancers<M> =>
function getDefaultEnhancers(options) {
const { autoBatch = true } = options ?? {}

let enhancerArray = new EnhancerArray<StoreEnhancer[]>(middlewareEnhancer)
let enhancerArray = new Tuple<StoreEnhancer[]>(middlewareEnhancer)
if (autoBatch) {
enhancerArray.push(
autoBatchEnhancer(typeof autoBatch === 'object' ? autoBatch : undefined)
Expand Down
6 changes: 3 additions & 3 deletions packages/toolkit/src/getDefaultMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { createImmutableStateInvariantMiddleware } from './immutableStateInvaria
import type { SerializableStateInvariantMiddlewareOptions } from './serializableStateInvariantMiddleware'
import { createSerializableStateInvariantMiddleware } from './serializableStateInvariantMiddleware'
import type { ExcludeFromTuple } from './tsHelpers'
import { MiddlewareArray } from './utils'
import { Tuple } from './utils'

function isBoolean(x: any): x is boolean {
return typeof x === 'boolean'
Expand Down Expand Up @@ -48,7 +48,7 @@ export type GetDefaultMiddleware<S = any> = <
}
>(
options?: O
) => MiddlewareArray<ExcludeFromTuple<[ThunkMiddlewareFor<S, O>], never>>
) => Tuple<ExcludeFromTuple<[ThunkMiddlewareFor<S, O>], never>>

export const buildGetDefaultMiddleware = <S = any>(): GetDefaultMiddleware<S> =>
function getDefaultMiddleware(options) {
Expand All @@ -59,7 +59,7 @@ export const buildGetDefaultMiddleware = <S = any>(): GetDefaultMiddleware<S> =>
actionCreatorCheck = true,
} = options ?? {}

let middlewareArray = new MiddlewareArray<Middleware[]>()
let middlewareArray = new Tuple<Middleware[]>()

if (thunk) {
if (isBoolean(thunk)) {
Expand Down
2 changes: 1 addition & 1 deletion packages/toolkit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export type {
// types
ActionReducerMapBuilder,
} from './mapBuilders'
export { MiddlewareArray, EnhancerArray } from './utils'
export { Tuple } from './utils'

export { createEntityAdapter } from './entities/create_adapter'
export type {
Expand Down
7 changes: 2 additions & 5 deletions packages/toolkit/src/query/tests/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import type {
Middleware,
Store,
Reducer,
EnhancerArray,
StoreEnhancer,
ThunkDispatch,
} from '@reduxjs/toolkit'
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
Expand Down Expand Up @@ -218,8 +215,8 @@ export function setupApiStore<
}).concat(api.middleware)

return tempMiddleware
.concat(...(middleware?.concat ?? []))
.prepend(...(middleware?.prepend ?? [])) as typeof tempMiddleware
.concat(middleware?.concat ?? [])
.prepend(middleware?.prepend ?? []) as typeof tempMiddleware
},
enhancers: (gde) =>
gde({
Expand Down
81 changes: 81 additions & 0 deletions packages/toolkit/src/tests/Tuple.typetest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Tuple } from '@reduxjs/toolkit'
import { expectType } from './helpers'

/**
* Test: compatibility is checked between described types
*/
{
const stringTuple = new Tuple('')

expectType<Tuple<[string]>>(stringTuple)

expectType<Tuple<string[]>>(stringTuple)

// @ts-expect-error
expectType<Tuple<[string, string]>>(stringTuple)

const numberTuple = new Tuple(0, 1)
// @ts-expect-error
expectType<Tuple<string[]>>(numberTuple)
}

/**
* Test: concat is inferred properly
*/
{
const singleString = new Tuple('')

expectType<Tuple<[string]>>(singleString)

expectType<Tuple<[string, string]>>(singleString.concat(''))

expectType<Tuple<[string, string]>>(singleString.concat(['']))
}

/**
* Test: prepend is inferred properly
*/
{
const singleString = new Tuple('')

expectType<Tuple<[string]>>(singleString)

expectType<Tuple<[string, string]>>(singleString.prepend(''))

expectType<Tuple<[string, string]>>(singleString.prepend(['']))
}

/**
* Test: push must match existing items
*/
{
const stringTuple = new Tuple('')

stringTuple.push('')

// @ts-expect-error
stringTuple.push(0)
}

/**
* Test: Tuples can be combined
*/
{
const stringTuple = new Tuple('')

const numberTuple = new Tuple(0, 1)

expectType<Tuple<[string, number, number]>>(stringTuple.concat(numberTuple))

expectType<Tuple<[number, number, string]>>(stringTuple.prepend(numberTuple))

expectType<Tuple<[number, number, string]>>(numberTuple.concat(stringTuple))

expectType<Tuple<[string, number, number]>>(numberTuple.prepend(stringTuple))

// @ts-expect-error
expectType<Tuple<[string, number, number]>>(stringTuple.prepend(numberTuple))

// @ts-expect-error
expectType<Tuple<[number, number, string]>>(stringTuple.concat(numberTuple))
}
Loading

0 comments on commit a2f3c9a

Please sign in to comment.