跳到主要内容

 

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 创建的, 但也可以是任何带有 reducerPathreducer 属性的 "类切片" 对象(这意味着 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 下挂载的切片。如果切片是在不同的键下挂载的,你可以将其声明为一个常规的键。

声明一个在其 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。

'移除'一个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中,确保任何当前注入的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',
})

selectors / getSelectors

类似于selector,来自"注入"的slice实例的选择器的行为稍有不同。

如果slice状态在传递的store状态中是未定义的,那么选择器将会被调用并传入slice的初始状态。

如果在注入过程中对reducerPath进行了更改,selectors也会反映这种更改。

console.log(
injectedCounterSlice.selectors.selectValue({}), // 0
injectedCounterSlice.selectors.selectValue({ counter: { value: 2 } }), // 2
aCounterSlice.selectors.selectValue({ aCounter: { value: 2 } }), // 2
)