combineSlices
概述
一个将切片组合成单个 reducer 的函数,并在初始化后启用更多的 reducer 注入。
// 文件: slices/api.ts noEmit
import type { Api } from '@reduxjs/toolkit/query'
export declare const api: Api<() => any, {}, 'api', never>
// 文件: slices/users.ts noEmit
import type { Slice } from '@reduxjs/toolkit'
export declare const userSlice: Slice<string, {}, 'user'>
// 文件: slices/index.ts
import { combineSlices } from '@reduxjs/toolkit'
import { api } from './api'
import { userSlice } from './users'
export const rootReducer = combineSlices(api, userSlice)
// 文件: store.ts
import { configureStore } from '@reduxjs/toolkit'
import { rootReducer } from './slices'
export const store = configureStore({
reducer: rootReducer,
})
combineSlices
的 "切片" 通常是用 createSlice
创建的,
但也可以是任何带有 reducerPath
和 reducer
属性的 "类切片" 对象(这意味着 RTK Query API 实例 也是兼容的)。
const withUserReducer = rootReducer.inject({
reducerPath: 'user',
reducer: userReducer,
})
const withApiReducer = rootReducer.inject(fooApi)
为了简单起见,这种 { reducerPath, reducer }
的形状在这些文档中将被描述为 "切片"。
参数
combineSlices
接受一组切片或 reducer 映射对象,并将它们组合成一个单一的 reducer。
切片将在其 reducerPath
下挂载,reducer 映射对象的项将在其各自的键下挂载。
const rootReducer = combineSlices(counterSlice, baseApi, {
user: userSlice.reducer,
auth: authSlice.reducer,
})
// 就像
const rootReducer = combineReducers({
[counterSlice.reducerPath]: counterSlice.reducer,
[baseApi.reducerPath]: baseApi.reducer,
user: userSlice.reducer,
auth: authSlice.reducer,
})
如果多个切片/映射对象有相同的 reducer 路径,后面 的参数提供的 reducer 将覆盖前面的。
然而,类型系统无法对此进行处理。最好确保所有的 reducer 都指向一个唯一的位置。
返回值
combineSlices
返回一个带有附加方法的 reducer 函数。
interface CombinedSliceReducer<InitialState, DeclaredState = InitialState>
extends Reducer<DeclaredState, AnyAction, Partial<DeclaredState>> {
withLazyLoadedSlices<LazyLoadedSlices>(): CombinedSliceReducer<
InitialState,
DeclaredState & Partial<LazyLoadedSlices>
>
inject<Slice extends SliceLike>(
slice: Slice,
config?: InjectConfig
): CombinedSliceReducer<InitialState, DeclaredState & WithSlice<Slice>>
selector: {
(selectorFn: Selector, selectState?: SelectFromRootState) => WrappedSelector
original(state: DeclaredState) => InitialState & Partial<DeclaredState>
}
}
withLazyLoadedSlices
建议从你的 store 推断你的 RootState 类型,这是从 reducer 推断出来的。然而,如果切片是懒加载的,那么就无法从中推断出来。
withLazyLoadedSlices
允许你声明将在后面添加到状态的切片,这将包含在最终的状态类型中。
管理这个的一个可能的模式是使用声明合并:
// 文件: slices/index.ts
import { combineSlices } from '@reduxjs/toolkit'
import { staticSlice } from './static'
export interface LazyLoadedSlices {}
export const rootReducer =
combineSlices(staticSlice).withLazyLoadedSlices<LazyLoadedSlices>()
// LazyLoadedSlices 中的键被标记为可选
export type RootState = ReturnType<typeof rootReducer>
// 文件: slices/lazySlice.ts
import type { WithSlice } from '@reduxjs/toolkit'
import { rootReducer } from '.'
const lazySlice = createSlice({
/* ... */
})
declare module '.' {
export interface LazyLoadedSlices extends WithSlice<typeof lazySlice> {}
}
const injectedReducer = rootReducer.inject(lazySlice)
// 和/或
const injectedSlice = lazySlice.injectInto(rootReducer)
上面的例子使用了 WithSlice
实用类型,用于在其 reducerPath
下挂载的切片。如果切片是在不同的键下挂载的,你可以将其声明为一个常规的键。
// 文件: slices/lazySlice.ts
import { rootReducer } from '.'
const lazySlice = createSlice({
/* ... */
})
declare module '.' {
export interface LazyLoadedSlices {
customKey: LazyState
}
}
const injectedReducer = rootReducer.inject({
reducerPath: 'customKey',
reducer: lazySlice.reducer,
})
// 和/或
const injectedSlice = lazySlice.injectInto(rootReducer, {
reducerPath: 'customKey',
})
inject
inject
允许你在初始化后向你的reducers集合中添加一个slice。它期望传入一个slice和一个可选的配置,然后返回一个包含该slice的更新后的reducer。
这主要用于懒加载reducers。
const reducerWithUser = rootReducer.inject(userSlice)
inject
将slice添加到你原始reducer中的reducers映射中,但不会派发一个动作。
这意味着,添加的reducer状态将不会在下一个动作被派发之前出现在你的store中。
Reducer替换
默认情况下,不允许替换reducer。在开发模式下,如果尝试将新的reducer实例注入到已经注入的reducerPath
中,将会在控制台中记录一个警告。(如果同一个reducer实例被注入到同一个位置两次,它不会警告。)
如果你希望允许用新的实例替换一个reducer,你必须明确地将overrideExisting: true
作为你的配置对象的一部分传入。
const reducerWithUser = rootReducer.inject(userSlice, {
overrideExisting: true,
})
这可能对于热重载或者通过替换它为一个总是返回null
的函数来"移除"一个reducer很有用。注意,为了可预测的行为,你的类型应该考虑到你打算占用一个路径的所有可能的reducers。
declare module '.' {
export interface LazyLoadedSlices {
removable: RemovableState | null
}
}
const withInjected = rootReducer.inject(
{ reducerPath: 'removable', reducer: removableReducer },
{ overrideExisting: true },
)
const emptyReducer = () => null
const removeReducer = () =>
rootReducer.inject(
{ reducerPath: 'removable', reducer: emptyReducer },
{ overrideExisting: true },
)
selector
如前所述,如果没有派发动作,注入的reducer在状态中仍然可能是未定义的。
在编写选择器时,处理这种可能的可选状态可能会很不方便,因为你可能会得到很多可能是未定义的结果,或者依赖于明确的默认值。
selector
允许你绕过这个问题,通过将reducer状态包装在一个Proxy
中,确保任何当前注入的reducers在状态中当前为undefined
时,它们会评估为它们的初始状态。
declare module '.' {
export interface LazyLoadedSlices extends WithSlice<typeof counterSlice> {}
}
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
/* ... */
},
})
const withCounter = rootReducer.inject(counterSlice)
const selectCounterValue = (rootState: RootState) => rootState.counter?.value // number | undefined
const wrappedSelectCounterValue = withCounter.selector(
(rootState) => rootState.counter.value, // number
)
console.log(
selectCounterValue({}), // undefined
selectCounterValue({ counter: { value: 2 } }), // 2
wrappedSelectCounterValue({}), // 0
wrappedSelectCounterValue({ counter: { value: 2 } }), // 2
)
Proxy
通过调用一个随机生成的动作类型来获取reducer的初始状态 - 不要试图在你的reducer内部处理这个作为一个特殊情况。
嵌套的组合reducer
包装的选择器期望使用组合reducer返回的状态作为其第一个参数。
如果组合reducer进一步嵌套在store状态中,将一个selectState
回调作为第二个参数传递给selector
:
interface RootState {
innerCombined: ReturnType<typeof combinedReducer>
}
const selectCounterValue = withCounter.selector(
(combinedState) => combinedState.counter.value,
(rootState: RootState) => rootState.innerCombined,
)
console.log(
selectCounterValue({
innerCombined: {},
}), // 0
selectCounterValue({
innerCombined: {
counter: {
value: 2,
},
},
}), // 2
)
original
类似于Immer使用,提 供了一个original
函数来获取提供给Proxy
的原始状态值。
这主要用于调试/检查,因为Proxy
实例往往以难以阅读的格式显示。
该函数作为selector
函数的一个方法附加:
const wrappedSelectCounterValue = withCounter.selector((rootState) => {
console.log(withCounter.selector.original(rootState))
return rootState.counter.value
})
Slice集成
injectInto
由createSlice
返回的Slice实例有一个附加的injectInto
方法,它接收一个来自combineSlices
的可注入reducer,并返回该slice的"注入"版本。
const injectedCounterSlice = counterSlice.injectInto(rootReducer)
可以传递一个可选的配置对象。这遵 循inject
的选项,并附加一个reducerPath
字段,用于将slice注入到除其当前reducerPath
属性之外的路径。
const aCounterSlice = counterSlice.injectInto(rootReducer, {
reducerPath: 'aCounter',
})