combineSlices
概述
一个将多个 slice 合并成单个 reducer 的函数,并允许在初始化后注入更多 reducer。
- TypeScript
- JavaScript
// file: slices/index.ts
import { combineSlices } from '@reduxjs/toolkit'
import { api } from './api'
import { userSlice } from './users'
export const rootReducer = combineSlices(api, userSlice)
// file: store.ts
import { configureStore } from '@reduxjs/toolkit'
import { rootReducer } from './slices'
export const store = configureStore({
reducer: rootReducer,
})
// file: slices/index.js
import { combineSlices } from '@reduxjs/toolkit'
import { api } from './api'
import { userSlice } from './users'
export const rootReducer = combineSlices(api, userSlice)
// file: store.js
import { configureStore } from '@reduxjs/toolkit'
import { rootReducer } from './slices'
export const store = configureStore({
reducer: rootReducer,
})
用于 combineSlices
的 "slice" 通常使用 createSlice
创建,但可以是任何具有 reducerPath
和 reducer
属性的 "slice-like" 对象(这意味着 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,
})
// is like
const rootReducer = combineReducers({
[counterSlice.reducerPath]: counterSlice.reducer,
[baseApi.reducerPath]: baseApi.reducer,
user: userSlice.reducer,
auth: authSlice.reducer,
})
如果多个切片/映射对象具有相同的 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
建议从你的 reducer 推断你的 RootState
类型,这可以通过 从你的 store 推断。但是,如果切片是延迟加载的,并且无法从 reducer 推断,这可能会导致问题。
withLazyLoadedSlices
允许你声明稍后将添加到状态的切片,这些切片将包含在最终状态类型中。
一种可能的管理模式是使用声明合并。
// file: slices/index.ts
import { combineSlices } from '@reduxjs/toolkit'
import { staticSlice } from './static'
export interface LazyLoadedSlices {}
export const rootReducer =
combineSlices(staticSlice).withLazyLoadedSlices<LazyLoadedSlices>()
// keys in LazyLoadedSlices are marked as optional
export type RootState = ReturnType<typeof rootReducer>
// file: 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)
// and/or
const injectedSlice = lazySlice.injectInto(rootReducer)
上面的示例使用 WithSlice
实用类型来表示安装在其 reducerPath
下的切片。如果切片安装在不同的键下,你可以将其声明为一个普通的键。
reducerPath
之外的切片// file: 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,
})
// and/or
const injectedSlice = lazySlice.injectInto(rootReducer, {
reducerPath: 'customKey',
})
inject
inject
允许您在初始化后将切片添加到您的 reducer 集合中。它期望传入一个切片和一个可选的配置,并返回一个包含该切片的 reducer 的更新版本。
这主要用于延迟加载 reducer。
const reducerWithUser = rootReducer.inject(userSlice)
inject
将切片添加到原始 reducer 中的 reducer 地图中,但不会分派操作。
这意味着添加的 reducer 状态在分派下一个操作之前不会出现在您的存储中。
Reducer 替换
默认情况下,不允许替换 reducer。在开发模式下,如果尝试将新的 reducer 实例注入到已经注入的 reducerPath
中,则会向控制台记录警告。(如果将相同的 reducer 实例两次注入到相同的位置,则不会发出警告。)
如果您希望允许用新的实例替换 reducer,则必须在配置对象中显式传递 overrideExisting: true
。
const reducerWithUser = rootReducer.inject(userSlice, {
overrideExisting: true,
})
这可能对热重载或通过用始终返回 null
的函数替换 reducer 来“删除”reducer 有用。请注意,为了获得可预测的行为,您的类型应考虑您打算占用路径的所有可能的 reducer。
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
中来解决这个问题,该 Proxy
确保任何当前注入的 reducer 如果当前在状态中为 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
通过使用随机生成的 action 类型调用 reducer 来检索 reducer 的初始状态 - 不要尝试在 reducer 内部将其作为特殊情况处理。
嵌套组合 reducer
包装的选择器期望使用组合 reducer 返回的状态作为其第一个参数。
如果组合 reducer 进一步嵌套在存储状态中,请将 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
})
切片集成
injectInto
由 createSlice
返回的切片实例具有一个附加的 injectInto
方法,该方法接收来自 combineSlices
的可注入 reducer 并返回该切片的“注入”版本。
const injectedCounterSlice = counterSlice.injectInto(rootReducer)
可以传递一个可选的配置对象。这遵循 inject
的选项,并具有一个额外的 reducerPath
字段,用于在除当前 reducerPath
属性之外的路径下注入切片。
const aCounterSlice = counterSlice.injectInto(rootReducer, {
reducerPath: 'aCounter',
})
selectors
/ getSelectors
类似于 selector
,来自“注入”切片实例的选择器行为略有不同。
如果在传递的存储状态中切片状态未定义,则选择器将改为使用切片的初始状态进行调用。
selectors
还将反映 reducerPath
的更改,如果在注入期间进行了更改。
console.log(
injectedCounterSlice.selectors.selectValue({}), // 0
injectedCounterSlice.selectors.selectValue({ counter: { value: 2 } }), // 2
aCounterSlice.selectors.selectValue({ aCounter: { value: 2 } }), // 2
)