Skip to content

Commit 38b9648

Browse files
committed
begin writing combineSlices section
1 parent 1cf40fa commit 38b9648

File tree

1 file changed

+233
-0
lines changed

1 file changed

+233
-0
lines changed

docs/usage/CodeSplitting.md

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,239 @@ To add a new reducer, one can now call `store.reducerManager.add("asyncState", a
154154

155155
To remove a reducer, one can now call `store.reducerManager.remove("asyncState")`
156156

157+
## Redux Toolkit
158+
159+
Redux Toolkit 2.0 includes some utilities designed to simplify code splitting with reducers and middleware, including solid Typescript support (a common challenge with lazy loaded reducers and middleware).
160+
161+
### `combineSlices`
162+
163+
The [`combineSlices`](https://redux-toolkit.js.org/api/combineSlices) utility is designed to allow for easy reducer injection. It also supercedes `combineReducers`, in that it can be used to combine multiple slices and reducers into one root reducer.
164+
165+
At setup it accepts a set of slices and reducer maps, and returns a reducer instance with attached methods for injection. **At least one reducer is required at setup, as with `combineReducers`.**
166+
167+
:::note
168+
169+
A "slice" for `combineSlices` is typically created with `createSlice`, but can be any "slice-like" object with `reducerPath` and `reducer` properties (meaning RTK Query API instances are also compatible).
170+
171+
```ts
172+
const withUserReducer = rootReducer.inject({
173+
reducerPath: 'user',
174+
reducer: userReducer
175+
})
176+
177+
const withApiReducer = rootReducer.inject(fooApi)
178+
```
179+
180+
For simplicity, this `{ reducerPath, reducer }` shape will be described in these docs as a "slice".
181+
182+
:::
183+
184+
Slices will be mounted at their `reducerPath`, and items from reducer map objects will be mounted under their respective key.
185+
186+
```ts
187+
const rootReducer = combineSlices(counterSlice, baseApi, {
188+
user: userSlice.reducer,
189+
auth: authSlice.reducer
190+
})
191+
// is like
192+
const rootReducer = combineReducers({
193+
[counterSlice.reducerPath]: counterSlice.reducer,
194+
[baseApi.reducerPath]: baseApi.reducer,
195+
user: userSlice.reducer,
196+
auth: authSlice.reducer
197+
})
198+
```
199+
200+
:::caution
201+
202+
Be careful to avoid naming collision - later keys will overwrite earlier ones, but Typescript won't be able to account for this.
203+
204+
:::
205+
206+
#### Slice injection
207+
208+
To inject a slice, you should call `rootReducer.inject(slice)` on the reducer instance returned from `combineSlices`. This will inject the slice under its `reducerPath` into the set of reducers, and return an instance of the combined reducer typed to know that the slice has been injected.
209+
210+
Alternatively, you can call `slice.injectInto(rootReducer)`, which returns an instance of the slice which is aware it's been injected. You may even want to do both, as each call returns something useful, and `combineSlices` allows injection of the same reducer instance at the same `reducerPath` without issue.
211+
212+
```ts
213+
const withCounterSlice = rootReducer.inject(counterSlice)
214+
const injectedCounterSlice = counterSlice.injectInto(rootReducer)
215+
```
216+
217+
One key difference between typical reducer injection and `combineSlice`'s "meta-reducer" approach is that `replaceReducer` is never called for `combineSlice`. The reducer instance passed to the store doesn't change.
218+
219+
A consequence of this is that no action is dispatched when a slice is injected, and therefore the injected slice's state doesn't show in state immediately. The state will only show in the store's state when an action is dispatched.
220+
221+
However, to avoid selectors having to account for possibly `undefined` state, `combineSlices` includes some useful [selector utilities](#selector-utilities).
222+
223+
#### Declaring lazy loaded slices
224+
225+
In order for lazy loaded slices to show up in the inferred state type, a `withLazyLoadedSlices` helper is provided. This allows you to declare slices you intend to later inject, so they can show up as optional in the state type.
226+
227+
To completely avoid importing the lazy slice into the combined reducer's file, module augmentation can be used.
228+
229+
```ts
230+
// file: reducer.ts
231+
import { combineSlices } from '@reduxjs/toolkit'
232+
import { staticSlice } from './staticSlice'
233+
234+
export interface LazyLoadedSlices {}
235+
236+
export const rootReducer =
237+
combineSlices(staticSlice).withLazyLoadedSlices<LazyLoadedSlices>()
238+
239+
// file: counterSlice.ts
240+
import type { WithSlice } from '@reduxjs/toolkit'
241+
import { createSlice } from '@reduxjs/toolkit'
242+
import { rootReducer } from './reducer'
243+
244+
interface CounterState {
245+
value: number
246+
}
247+
248+
const counterSlice = createSlice({
249+
name: 'counter',
250+
initialState: { value: 0 } as CounterState,
251+
reducers: {
252+
increment: state => void state.value++
253+
},
254+
selectors: {
255+
selectValue: state => state.value
256+
}
257+
})
258+
259+
declare module './reducer' {
260+
// WithSlice utility assumes reducer is under slice.reducerPath
261+
export interface LazyLoadedSlices extends WithSlice<typeof counterSlice> {}
262+
263+
// if it's not, just use a normal key
264+
export interface LazyLoadedSlices {
265+
aCounter: CounterState
266+
}
267+
}
268+
269+
const injectedCounterSlice = counterSlice.injectInto(rootReducer)
270+
const injectedACounterSlice = counterSlice.injectInto(rootReducer, {
271+
reducerPath: 'aCounter'
272+
})
273+
```
274+
275+
#### Selector utilities
276+
277+
As well as `inject`, the combined reducer instance has a `.selector` method which can be used to wrap selectors. It wraps the state object in a `Proxy`, and provides an initial state for any reducers which have been injected but haven't appeared in state yet.
278+
279+
The result of calling `inject` is typed to know that the injected slice will always be defined when the selector is called.
280+
281+
```ts
282+
const selectCounterValue = (state: RootState) => state.counter?.value // number | undefined
283+
284+
const withCounterSlice = rootReducer.inject(counterSlice)
285+
const selectCounterValue = withCounterSlice.selector(
286+
state => state.counter.value // number - initial state used if not in store
287+
)
288+
```
289+
290+
An "injected" instance of a slice will do the same thing for slice selectors - initial state will be provided if not present in the state passed.
291+
292+
```ts
293+
const injectedCounterSlice = counterSlice.injectInto(rootReducer)
294+
295+
console.log(counterSlice.selectors.selectValue({})) // runtime error
296+
console.log(injectedCounterSlice.selectors.selectValue({})) // 0
297+
```
298+
299+
#### Typical usage
300+
301+
`combineSlices` is designed so that the slice is injected as soon as it's needed (i.e. a selector or action is imported from a component that's been loaded in).
302+
303+
This means that the typical usage will look something along the lines of the below.
304+
305+
```ts
306+
// file: reducer.ts
307+
import { combineSlices } from '@reduxjs/toolkit'
308+
import { staticSlice } from './staticSlice'
309+
310+
export interface LazyLoadedSlices {}
311+
312+
export const rootReducer =
313+
combineSlices(staticSlice).withLazyLoadedSlices<LazyLoadedSlices>()
314+
315+
// file: store.ts
316+
import { configureStore } from '@reduxjs/toolkit'
317+
import { rootReducer } from './reducer'
318+
319+
export const store = configureStore({ reducer: rootReducer })
320+
321+
// file: counterSlice.ts
322+
import type { WithSlice } from '@reduxjs/toolkit'
323+
import { createSlice } from '@reduxjs/toolkit'
324+
import { rootReducer } from './reducer'
325+
326+
const counterSlice = createSlice({
327+
name: 'counter',
328+
initialState: { value: 0 },
329+
reducers: {
330+
increment: state => void state.value++
331+
},
332+
selectors: {
333+
selectValue: state => state.value
334+
}
335+
})
336+
337+
export const { increment } = counterSlice.actions
338+
339+
declare module './reducer' {
340+
export interface LazyLoadedSlices extends WithSlice<typeof counterSlice> {}
341+
}
342+
343+
const injectedCounterSlice = counterSlice.injectInto(rootReducer)
344+
345+
export const { selectValue } = injectedCounterSlice.selectors
346+
347+
// file: Counter.tsx
348+
// by importing from counterSlice we guarantee
349+
// the injection happens before this component is defined
350+
import { increment, selectValue } from './counterSlice'
351+
import { useAppDispatch, useAppSelector } from './hooks'
352+
353+
export default function Counter() {
354+
const dispatch = usAppDispatch()
355+
const value = useAppSelector(selectValue)
356+
return (
357+
<>
358+
<p>{value}</p>
359+
<button onClick={() => dispatch(increment())}>Increment</button>
360+
</>
361+
)
362+
}
363+
364+
// file: App.tsx
365+
import { Provider } from 'react-redux'
366+
import { store } from './store'
367+
368+
// lazily importing the component means that the code
369+
// doesn't actually get pulled in and executed until the component is rendered.
370+
// this means that the inject call only happens once Counter renders
371+
const Counter = React.lazy(() => import('./Counter'))
372+
373+
function App() {
374+
return (
375+
<Provider store={store}>
376+
<Counter />
377+
</Provider>
378+
)
379+
}
380+
```
381+
382+
### `createDynamicMiddleware`
383+
384+
#### `addMiddleware`
385+
386+
#### `withMiddleware`
387+
388+
#### React integration
389+
157390
## Libraries and Frameworks
158391

159392
There are a few good libraries out there that can help you add the above functionality automatically:

0 commit comments

Comments
 (0)