Skip to content

Commit

Permalink
Merge pull request #3207 from EskiMojo14/enhancer-array
Browse files Browse the repository at this point in the history
Fixes #3206
  • Loading branch information
markerikson committed Apr 18, 2023
2 parents da3f6ed + 5ad74aa commit 1f87ac2
Show file tree
Hide file tree
Showing 8 changed files with 327 additions and 19 deletions.
9 changes: 5 additions & 4 deletions docs/api/configureStore.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ to the store setup for a better development experience.

```ts no-transpile
type ConfigureEnhancersCallback = (
defaultEnhancers: StoreEnhancer[]
defaultEnhancers: EnhancerArray<[StoreEnhancer]>
) => StoreEnhancer[]

interface ConfigureStoreOptions<
Expand Down Expand Up @@ -107,7 +107,8 @@ a list of the specific options that are available.
Defaults to `true`.

#### `trace`
The Redux DevTools Extension recently added [support for showing action stack traces](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/Features/Trace.md) that show exactly where each action was dispatched.

The Redux DevTools Extension recently added [support for showing action stack traces](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/Features/Trace.md) that show exactly where each action was dispatched.
Capturing the traces can add a bit of overhead, so the DevTools Extension allows users to configure whether action stack traces are captured by [setting the 'trace' argument](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md#trace).
If the DevTools are enabled by passing `true` or an object, then `configureStore` will default to enabling capturing action stack traces in development mode only.

Expand All @@ -129,7 +130,7 @@ If defined as a callback function, it will be called with the existing array of
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`.

Example: `enhancers: (defaultEnhancers) => [offline, ...defaultEnhancers]` will result in a final setup
Example: `enhancers: (defaultEnhancers) => defaultEnhancers.prepend(offline)` will result in a final setup
of `[offline, applyMiddleware, devToolsExtension]`.

## Usage
Expand Down Expand Up @@ -195,7 +196,7 @@ const preloadedState = {
visibilityFilter: 'SHOW_COMPLETED',
}
const debounceNotify = _.debounce(notify => notify());
const debounceNotify = _.debounce((notify) => notify())
const store = configureStore({
reducer,
Expand Down
16 changes: 10 additions & 6 deletions packages/toolkit/src/configureStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import type {
NoInfer,
ExtractDispatchExtensions,
ExtractStoreExtensions,
ExtractStateExtensions,
} from './tsHelpers'
import { EnhancerArray } from './utils'

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

Expand All @@ -34,8 +36,8 @@ const IS_PRODUCTION = process.env.NODE_ENV === 'production'
* @public
*/
export type ConfigureEnhancersCallback<E extends Enhancers = Enhancers> = (
defaultEnhancers: readonly StoreEnhancer[]
) => [...E]
defaultEnhancers: EnhancerArray<[StoreEnhancer<{}, {}>]>
) => E

/**
* Options for `configureStore()`.
Expand Down Expand Up @@ -107,7 +109,7 @@ type Enhancers = ReadonlyArray<StoreEnhancer>
export interface ToolkitStore<
S = any,
A extends Action = AnyAction,
M extends Middlewares<S> = Middlewares<S>,
M extends Middlewares<S> = Middlewares<S>
> extends Store<S, A> {
/**
* The `dispatch` method of your store, enhanced by all its middlewares.
Expand All @@ -128,7 +130,8 @@ export type EnhancedStore<
A extends Action = AnyAction,
M extends Middlewares<S> = Middlewares<S>,
E extends Enhancers = Enhancers
> = ToolkitStore<S, A, M> & ExtractStoreExtensions<E>
> = ToolkitStore<S & ExtractStateExtensions<E>, A, M> &
ExtractStoreExtensions<E>

/**
* A friendly abstraction over the standard Redux `createStore()` function.
Expand Down Expand Up @@ -197,12 +200,13 @@ export function configureStore<
})
}

let storeEnhancers: Enhancers = [middlewareEnhancer]
const defaultEnhancers = new EnhancerArray(middlewareEnhancer)
let storeEnhancers: Enhancers = defaultEnhancers

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

const composedEnhancer = finalCompose(...storeEnhancers) as StoreEnhancer<any>
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 @@ -105,7 +105,7 @@ export type {
// types
ActionReducerMapBuilder,
} from './mapBuilders'
export { MiddlewareArray } from './utils'
export { MiddlewareArray, EnhancerArray } from './utils'

export { createEntityAdapter } from './entities/create_adapter'
export type {
Expand Down
135 changes: 135 additions & 0 deletions packages/toolkit/src/tests/EnhancerArray.typetest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { configureStore } from '@reduxjs/toolkit'
import type { StoreEnhancer } from 'redux'

declare const expectType: <T>(t: T) => T

declare const enhancer1: StoreEnhancer<
{
has1: true
},
{ stateHas1: true }
>

declare const enhancer2: StoreEnhancer<
{
has2: true
},
{ stateHas2: true }
>

{
// prepend single element
{
const store = configureStore({
reducer: () => 0,
enhancers: (dE) => dE.prepend(enhancer1),
})
expectType<true>(store.has1)
expectType<true>(store.getState().stateHas1)

// @ts-expect-error
expectType<true>(store.has2)
// @ts-expect-error
expectType<true>(store.getState().stateHas2)
}

// prepend multiple (rest)
{
const store = configureStore({
reducer: () => 0,
enhancers: (dE) => dE.prepend(enhancer1, enhancer2),
})
expectType<true>(store.has1)
expectType<true>(store.getState().stateHas1)
expectType<true>(store.has2)
expectType<true>(store.getState().stateHas2)

// @ts-expect-error
expectType<true>(store.has3)
// @ts-expect-error
expectType<true>(store.getState().stateHas3)
}

// prepend multiple (array notation)
{
const store = configureStore({
reducer: () => 0,
enhancers: (dE) => dE.prepend([enhancer1, enhancer2] as const),
})
expectType<true>(store.has1)
expectType<true>(store.getState().stateHas1)
expectType<true>(store.has2)
expectType<true>(store.getState().stateHas2)

// @ts-expect-error
expectType<true>(store.has3)
// @ts-expect-error
expectType<true>(store.getState().stateHas3)
}

// concat single element
{
const store = configureStore({
reducer: () => 0,
enhancers: (dE) => dE.concat(enhancer1),
})
expectType<true>(store.has1)
expectType<true>(store.getState().stateHas1)

// @ts-expect-error
expectType<true>(store.has2)
// @ts-expect-error
expectType<true>(store.getState().stateHas2)
}

// prepend multiple (rest)
{
const store = configureStore({
reducer: () => 0,
enhancers: (dE) => dE.concat(enhancer1, enhancer2),
})
expectType<true>(store.has1)
expectType<true>(store.getState().stateHas1)
expectType<true>(store.has2)
expectType<true>(store.getState().stateHas2)

// @ts-expect-error
expectType<true>(store.has3)
// @ts-expect-error
expectType<true>(store.getState().stateHas3)
}

// concat multiple (array notation)
{
const store = configureStore({
reducer: () => 0,
enhancers: (dE) => dE.concat([enhancer1, enhancer2] as const),
})
expectType<true>(store.has1)
expectType<true>(store.getState().stateHas1)
expectType<true>(store.has2)
expectType<true>(store.getState().stateHas2)

// @ts-expect-error
expectType<true>(store.has3)
// @ts-expect-error
expectType<true>(store.getState().stateHas3)
}

// concat and prepend
{
const store = configureStore({
reducer: () => 0,
enhancers: (dE) => dE.concat(enhancer1).prepend(enhancer2),
})
expectType<true>(store.has1)
expectType<true>(store.getState().stateHas1)
expectType<true>(store.has2)
expectType<true>(store.getState().stateHas2)

// @ts-expect-error
expectType<true>(store.has3)
// @ts-expect-error
expectType<true>(store.getState().stateHas3)
}
}
4 changes: 1 addition & 3 deletions packages/toolkit/src/tests/configureStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,7 @@ describe('configureStore', () => {

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

expect(dummyEnhancerCalled).toBe(true)
Expand Down
80 changes: 80 additions & 0 deletions packages/toolkit/src/tests/configureStore.typetest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,86 @@ const _anyMiddleware: any = () => () => () => {}
)
expectType<string>(store.someProperty)
expectType<number>(store.anotherProperty)

const storeWithCallback = configureStore({
reducer: () => 0,
enhancers: (defaultEnhancers) =>
defaultEnhancers
.prepend(anotherPropertyStoreEnhancer)
.concat(somePropertyStoreEnhancer),
})

expectType<Dispatch & ThunkDispatch<number, undefined, AnyAction>>(
store.dispatch
)
expectType<string>(storeWithCallback.someProperty)
expectType<number>(storeWithCallback.anotherProperty)
}

{
type StateExtendingEnhancer = StoreEnhancer<{}, { someProperty: string }>

const someStateExtendingEnhancer: StateExtendingEnhancer =
(next) =>
// @ts-expect-error how do you properly return an enhancer that extends state?
(...args) => {
const store = next(...args)
const getState = () => ({
...store.getState(),
someProperty: 'some value',
})
return {
...store,
getState,
}
}

type AnotherStateExtendingEnhancer = StoreEnhancer<
{},
{ anotherProperty: number }
>

const anotherStateExtendingEnhancer: AnotherStateExtendingEnhancer =
(next) =>
// @ts-expect-error any input on this would be great
(...args) => {
const store = next(...args)
const getState = () => ({
...store.getState(),
anotherProperty: 123,
})
return {
...store,
getState,
}
}

const store = configureStore({
reducer: () => ({ aProperty: 0 }),
enhancers: [
someStateExtendingEnhancer,
anotherStateExtendingEnhancer,
// this doesn't work without the as const
] as const,
})

const state = store.getState()

expectType<number>(state.aProperty)
expectType<string>(state.someProperty)
expectType<number>(state.anotherProperty)

const storeWithCallback = configureStore({
reducer: () => ({ aProperty: 0 }),
enhancers: (dE) =>
dE.concat(someStateExtendingEnhancer, anotherStateExtendingEnhancer),
})

const stateWithCallback = storeWithCallback.getState()

expectType<number>(stateWithCallback.aProperty)
expectType<string>(stateWithCallback.someProperty)
expectType<number>(stateWithCallback.anotherProperty)
}
}

Expand Down
Loading

0 comments on commit 1f87ac2

Please sign in to comment.