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

Allow inference of enhancer state extensions, and fix inference when using callback form #3207

Merged
merged 3 commits into from
Apr 18, 2023
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
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 @@ -103,7 +103,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