UNPKG

@reduxjs/toolkit

Version:

The official, opinionated, batteries-included toolset for efficient Redux development

696 lines (613 loc) 18 kB
/* eslint-disable no-lone-blocks */ import type { Dispatch, AnyAction, Middleware, Reducer, Store, Action, StoreEnhancer, } from 'redux' import { applyMiddleware } from 'redux' import type { PayloadAction, ConfigureStoreOptions } from '@reduxjs/toolkit' import { configureStore, getDefaultMiddleware, createSlice, } from '@reduxjs/toolkit' import type { ThunkMiddleware, ThunkAction, ThunkDispatch } from 'redux-thunk' import thunk from 'redux-thunk' import { expectNotAny, expectType } from './helpers' const _anyMiddleware: any = () => () => () => {} /* * Test: configureStore() requires a valid reducer or reducer map. */ { configureStore({ reducer: (state, action) => 0, }) configureStore({ reducer: { counter1: () => 0, counter2: () => 1, }, }) // @ts-expect-error configureStore({ reducer: 'not a reducer' }) // @ts-expect-error configureStore({ reducer: { a: 'not a reducer' } }) // @ts-expect-error configureStore({}) } /* * Test: configureStore() infers the store state type. */ { const reducer: Reducer<number> = () => 0 const store = configureStore({ reducer }) const numberStore: Store<number, AnyAction> = store // @ts-expect-error const stringStore: Store<string, AnyAction> = store } /* * Test: configureStore() infers the store action type. */ { const reducer: Reducer<number, PayloadAction<number>> = () => 0 const store = configureStore({ reducer }) const numberStore: Store<number, PayloadAction<number>> = store // @ts-expect-error const stringStore: Store<number, PayloadAction<string>> = store } /* * Test: configureStore() accepts middleware array. */ { const middleware: Middleware = (store) => (next) => next configureStore({ reducer: () => 0, middleware: [middleware], }) configureStore({ reducer: () => 0, // @ts-expect-error middleware: ['not middleware'], }) } /* * Test: configureStore() accepts devTools flag. */ { configureStore({ reducer: () => 0, devTools: true, }) configureStore({ reducer: () => 0, // @ts-expect-error devTools: 'true', }) } /* * Test: configureStore() accepts devTools EnhancerOptions. */ { configureStore({ reducer: () => 0, devTools: { name: 'myApp' }, }) configureStore({ reducer: () => 0, // @ts-expect-error devTools: { appname: 'myApp' }, }) } /* * Test: configureStore() accepts preloadedState. */ { configureStore({ reducer: () => 0, preloadedState: 0, }) configureStore({ reducer: () => 0, // @ts-expect-error preloadedState: 'non-matching state type', }) } /* * Test: configureStore() accepts store enhancer. */ { { const store = configureStore({ reducer: () => 0, enhancers: [applyMiddleware(() => (next) => next)], }) expectType<Dispatch & ThunkDispatch<number, undefined, AnyAction>>( store.dispatch ) } configureStore({ reducer: () => 0, // @ts-expect-error enhancers: ['not a store enhancer'], }) { type SomePropertyStoreEnhancer = StoreEnhancer<{ someProperty: string }> const somePropertyStoreEnhancer: SomePropertyStoreEnhancer = (next) => { return (reducer, preloadedState) => { return { ...next(reducer, preloadedState), someProperty: 'some value', } } } type AnotherPropertyStoreEnhancer = StoreEnhancer<{ anotherProperty: number }> const anotherPropertyStoreEnhancer: AnotherPropertyStoreEnhancer = ( next ) => { return (reducer, preloadedState) => { return { ...next(reducer, preloadedState), anotherProperty: 123, } } } const store = configureStore({ reducer: () => 0, enhancers: [somePropertyStoreEnhancer, anotherPropertyStoreEnhancer], }) expectType<Dispatch & ThunkDispatch<number, undefined, AnyAction>>( store.dispatch ) 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) } } /** * Test: configureStore() state type inference works when specifying both a * reducer object and a partial preloaded state. */ { let counterReducer1: Reducer<number> = () => 0 let counterReducer2: Reducer<number> = () => 0 const store = configureStore({ reducer: { counter1: counterReducer1, counter2: counterReducer2, }, preloadedState: { counter1: 0, }, }) const counter1: number = store.getState().counter1 const counter2: number = store.getState().counter2 } /** * 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: by default, dispatching Thunks is possible */ { const store = configureStore({ reducer: reducerA, }) store.dispatch(thunkA()) // @ts-expect-error store.dispatch(thunkB()) const res = store.dispatch((dispatch, getState) => { return 42 }) const action = store.dispatch({ type: 'foo' }) } /** * Test: return type of thunks and actions is inferred correctly */ { const slice = createSlice({ name: 'counter', initialState: { value: 0, }, reducers: { incrementByAmount: (state, action: PayloadAction<number>) => { state.value += action.payload }, }, }) const store = configureStore({ reducer: { counter: slice.reducer, }, }) const action = slice.actions.incrementByAmount(2) const dispatchResult = store.dispatch(action) expectType<{ type: string; payload: number }>(dispatchResult) const promiseResult = store.dispatch(async (dispatch) => { return 42 }) expectType<Promise<number>>(promiseResult) const store2 = configureStore({ reducer: { counter: slice.reducer, }, middleware: (gDM) => gDM({ thunk: { extraArgument: 42, }, }), }) const dispatchResult2 = store2.dispatch(action) expectType<{ type: string; payload: number }>(dispatchResult2) } /** * Test: removing the Thunk Middleware */ { const store = configureStore({ reducer: reducerA, middleware: [], }) // @ts-expect-error store.dispatch(thunkA()) // @ts-expect-error store.dispatch(thunkB()) } /** * Test: adding the thunk middleware by hand */ { const store = configureStore({ reducer: reducerA, middleware: [thunk] as [ThunkMiddleware<StateA>], }) store.dispatch(thunkA()) // @ts-expect-error store.dispatch(thunkB()) } /** * Test: using getDefaultMiddleware */ { const store = configureStore({ reducer: reducerA, middleware: getDefaultMiddleware<StateA>(), }) store.dispatch(thunkA()) // @ts-expect-error store.dispatch(thunkB()) } /** * Test: custom middleware */ { const store = configureStore({ reducer: reducerA, middleware: [] as any as [Middleware<(a: StateA) => boolean, StateA>], }) const result: boolean = store.dispatch(5) // @ts-expect-error const result2: string = store.dispatch(5) } /** * Test: read-only middleware tuple */ { const store = configureStore({ reducer: reducerA, middleware: [] as any as readonly [ Middleware<(a: StateA) => boolean, StateA> ], }) const result: boolean = store.dispatch(5) // @ts-expect-error const result2: string = store.dispatch(5) } /** * Test: multiple custom middleware */ { const middleware = [] as any as [ Middleware<(a: 'a') => 'A', StateA>, Middleware<(b: 'b') => 'B', StateA>, ThunkMiddleware<StateA> ] const store = configureStore({ reducer: reducerA, middleware, }) const result: 'A' = store.dispatch('a') const result2: 'B' = store.dispatch('b') const result3: Promise<'A'> = store.dispatch(thunkA()) } /** * Accepts thunk with `unknown`, `undefined` or `null` ThunkAction extraArgument per default */ { const store = configureStore({ reducer: {} }) // undefined is the default value for the ThunkMiddleware extraArgument store.dispatch(function () {} as ThunkAction< void, {}, undefined, AnyAction >) // `null` for the `extra` generic was previously documented in the RTK "Advanced Tutorial", but // is a bad pattern and users should use `unknown` instead // @ts-expect-error store.dispatch(function () {} as ThunkAction<void, {}, null, AnyAction>) // unknown is the best way to type a ThunkAction if you do not care // about the value of the extraArgument, as it will always work with every // ThunkMiddleware, no matter the actual extraArgument type store.dispatch(function () {} as ThunkAction<void, {}, unknown, AnyAction>) // @ts-expect-error store.dispatch(function () {} as ThunkAction<void, {}, boolean, AnyAction>) } /** * Test: custom middleware and getDefaultMiddleware */ { const middleware = getDefaultMiddleware<StateA>().prepend( (() => {}) as any as Middleware<(a: 'a') => 'A', StateA> ) const store = configureStore({ reducer: reducerA, middleware, }) const result1: 'A' = store.dispatch('a') const result2: Promise<'A'> = store.dispatch(thunkA()) // @ts-expect-error store.dispatch(thunkB()) } /** * Test: custom middleware and getDefaultMiddleware, using prepend */ { const otherMiddleware: Middleware<(a: 'a') => 'A', StateA> = _anyMiddleware const concatenated = getDefaultMiddleware<StateA>().prepend(otherMiddleware) expectType< ReadonlyArray<typeof otherMiddleware | ThunkMiddleware | Middleware<{}>> >(concatenated) const store = configureStore({ reducer: reducerA, middleware: concatenated, }) const result1: 'A' = store.dispatch('a') const result2: Promise<'A'> = store.dispatch(thunkA()) // @ts-expect-error store.dispatch(thunkB()) } /** * Test: custom middleware and getDefaultMiddleware, using concat */ { const otherMiddleware: Middleware<(a: 'a') => 'A', StateA> = _anyMiddleware const concatenated = getDefaultMiddleware<StateA>().concat(otherMiddleware) expectType< ReadonlyArray<typeof otherMiddleware | ThunkMiddleware | Middleware<{}>> >(concatenated) const store = configureStore({ reducer: reducerA, middleware: concatenated, }) const result1: 'A' = store.dispatch('a') const result2: Promise<'A'> = store.dispatch(thunkA()) // @ts-expect-error store.dispatch(thunkB()) } /** * Test: middlewareBuilder notation, getDefaultMiddleware (unconfigured) */ { const store = configureStore({ reducer: reducerA, middleware: (getDefaultMiddleware) => getDefaultMiddleware().prepend((() => {}) as any as Middleware< (a: 'a') => 'A', StateA >), }) const result1: 'A' = store.dispatch('a') const result2: Promise<'A'> = store.dispatch(thunkA()) // @ts-expect-error store.dispatch(thunkB()) } /** * Test: middlewareBuilder notation, getDefaultMiddleware, concat & prepend */ { const otherMiddleware: Middleware<(a: 'a') => 'A', StateA> = _anyMiddleware const otherMiddleware2: Middleware<(a: 'b') => 'B', StateA> = _anyMiddleware const store = configureStore({ reducer: reducerA, middleware: (getDefaultMiddleware) => getDefaultMiddleware() .concat(otherMiddleware) .prepend(otherMiddleware2), }) const result1: 'A' = store.dispatch('a') const result2: Promise<'A'> = store.dispatch(thunkA()) const result3: 'B' = store.dispatch('b') // @ts-expect-error store.dispatch(thunkB()) } /** * Test: middlewareBuilder notation, getDefaultMiddleware (thunk: false) */ { const store = configureStore({ reducer: reducerA, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ thunk: false }).prepend( (() => {}) as any as Middleware<(a: 'a') => 'A', StateA> ), }) const result1: 'A' = store.dispatch('a') // @ts-expect-error store.dispatch(thunkA()) } /** * Test: badly typed middleware won't make `dispatch` `any` */ { const store = configureStore({ reducer: reducerA, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(_anyMiddleware as Middleware<any>), }) expectNotAny(store.dispatch) } /** * Test: decorated `configureStore` won't make `dispatch` `never` */ { const someSlice = createSlice({ name: 'something', initialState: null as any, reducers: { set(state) { return state }, }, }) function configureMyStore<S>( options: Omit<ConfigureStoreOptions<S>, 'reducer'> ) { return configureStore({ ...options, reducer: someSlice.reducer, }) } const store = configureMyStore({}) expectType<Function>(store.dispatch) } { interface CounterState { value: number } const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 } as CounterState, reducers: { increment(state) { state.value += 1 }, decrement(state) { state.value -= 1 }, // Use the PayloadAction type to declare the contents of `action.payload` incrementByAmount: (state, action: PayloadAction<number>) => { state.value += action.payload }, }, }) type Unsubscribe = () => void // A fake middleware that tells TS that an unsubscribe callback is being returned for a given action // This is the same signature that the "listener" middleware uses const dummyMiddleware: Middleware< { (action: Action<'actionListenerMiddleware/add'>): Unsubscribe }, CounterState > = (storeApi) => (next) => (action) => {} const store = configureStore({ reducer: counterSlice.reducer, middleware: (gDM) => gDM().prepend(dummyMiddleware), }) // Order matters here! We need the listener type to come first, otherwise // the thunk middleware type kicks in and TS thinks a plain action is being returned expectType< ((action: Action<'actionListenerMiddleware/add'>) => Unsubscribe) & ThunkDispatch<CounterState, undefined, AnyAction> & Dispatch<AnyAction> >(store.dispatch) const unsubscribe = store.dispatch({ type: 'actionListenerMiddleware/add', } as const) expectType<Unsubscribe>(unsubscribe) } }