跳至主要内容

combineSlices

概述

一个将多个 slice 合并成单个 reducer 的函数,并允许在初始化后注入更多 reducer。

// 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,
})
注意

用于 combineSlices 的 "slice" 通常使用 createSlice 创建,但可以是任何具有 reducerPathreducer 属性的 "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。

通过用无操作函数替换 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
)