Skip to content

Commit 8d61a90

Browse files
committed
Merge branch 'create-slice-creators' into entity-methods-creator
2 parents 17cad3c + a84abbd commit 8d61a90

File tree

2 files changed

+121
-16
lines changed

2 files changed

+121
-16
lines changed

docs/usage/custom-slice-creators.mdx

Lines changed: 114 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,8 @@ The same as [`addCase`](../api/createReducer#builderaddcase) for `createReducer`
544544
```ts no-transpile
545545
const action = createAction(type)
546546
context.addCase(action, reducer)
547+
// or
548+
context.addCase(type, reducer)
547549
```
548550

549551
#### `addMatcher`
@@ -564,6 +566,12 @@ const action = createAction(type)
564566
context.exposeAction(action)
565567
```
566568

569+
:::caution
570+
571+
`exposeAction` should only be called once (at maximum) within a `handle` callback - the same applies to `exposeCaseReducer`.
572+
573+
:::
574+
567575
#### `exposeCaseReducer`
568576

569577
Attaches a value to `slice.caseReducers[reducerName]`.
@@ -577,12 +585,12 @@ context.exposeCaseReducer(reducer)
577585
Returns the initial state value for the slice. If a lazy state initializer has been provided, it will be called and a fresh value returned.
578586

579587
```ts no-transpile
580-
const resetAction = createAction(type)
588+
const resetAction = createAction(type + '/reset')
581589
const resetReducer = () => context.getInitialState()
582590
context
583591
.addCase(resetAction, resetReducer)
584-
.exposeAction(resetAction)
585-
.exposeCaseReducer(resetReducer)
592+
.exposeAction({ reset: resetAction })
593+
.exposeCaseReducer({ reset: resetReducer })
586594
```
587595

588596
#### `selectSlice`
@@ -602,7 +610,7 @@ const aThunk =
602610

603611
The Typescript system for custom slice creators uses a "creator registry" system similar to the module system for [RTK Query](/rtk-query/usage/customizing-create-api#creating-your-own-module).
604612

605-
Creators are registered by using module augmentation to add a new key (their unique `type`) to the `SliceReducerCreators` interface. The interface receives three type parameters (`State`, `CaseReducers` and `Name`), and each entry should use the `ReducerCreatorEntry` type utility.
613+
Creators are registered by using module augmentation to add a new key (their unique `type`) to the `SliceReducerCreators` interface. Each entry should use the `ReducerCreatorEntry` type utility.
606614

607615
```ts no-transpile
608616
const reducerCreatorType = Symbol('reducerCreatorType')
@@ -634,9 +642,9 @@ The `ReducerCreatorEntry<Create, Exposes>` utility has two type parameters:
634642

635643
The signature of the `create` method of the creator definition.
636644

637-
:::caution `CaseReducers` and `Name`
645+
:::caution `CaseReducers`
638646

639-
Your `Create` type should not depend on the `CaseReducers` and `Name` type parameters, as these will not yet exist when the creator is being called.
647+
Your `Create` type should not depend on the `CaseReducers` type parameter, as these will not yet exist when the creator is being called.
640648

641649
:::
642650

@@ -679,6 +687,39 @@ const batchedCreator: ReducerCreator<typeof batchedCreatorType> = {
679687

680688
The second argument to the `ReducerCreators` type is a map from creator names to types, which you should supply if you're expecting to use any custom creators (anything other than `reducer` and `preparedReducer`) within your own creator. For example, `ReducerCreators<State, { asyncThunk: typeof asyncThunkCreator.type }>` would allow you to call `this.asyncThunk`.
681689

690+
Alternatively, you can import the other creator's definition and use it directly.
691+
692+
```ts no-transpile
693+
import { preparedReducerCreator } from '@reduxjs/toolkit'
694+
695+
const batchedCreatorType = Symbol('batchedCreatorType')
696+
697+
declare module '@reduxjs/toolkit' {
698+
export interface SliceReducerCreators<
699+
State,
700+
CaseReducers extends CreatorCaseReducers<State>,
701+
Name extends string,
702+
ReducerPath extends string,
703+
> {
704+
[batchedCreatorType]: ReducerCreatorEntry<
705+
<Payload>(
706+
reducer: CaseReducer<State, PayloadAction<Payload>>,
707+
) => PreparedCaseReducerDefinition<
708+
State,
709+
(payload: Payload) => { payload: Payload; meta: unknown }
710+
>
711+
>
712+
}
713+
}
714+
715+
const batchedCreator: ReducerCreator<typeof batchedCreatorType> = {
716+
type: batchedCreatorType,
717+
create(reducer) {
718+
return preparedReducerCreator.create(prepareAutoBatched(), reducer)
719+
},
720+
}
721+
```
722+
682723
:::
683724

684725
:::note Ensuring compatible state
@@ -752,6 +793,8 @@ In order to ensure that the definitions are correctly filtered to only include t
752793
}
753794
```
754795

796+
To relate back to the context methods, it should describe what you will pass to `context.exposeAction` from a handler.
797+
755798
For example, with (a simplified version of) the `asyncThunk` creator:
756799

757800
```ts no-transpile
@@ -790,6 +833,8 @@ declare module '@reduxjs/toolkit' {
790833

791834
Similar to `actions`, except for `slice.caseReducers`.
792835

836+
It describes what you will pass to `context.exposeCaseReducer` from a handler.
837+
793838
For example, with the `preparedReducer` creator:
794839

795840
```ts no-transpile
@@ -1055,6 +1100,8 @@ const paginationCreator: ReducerCreator<typeof paginationCreatorType> = {
10551100
type: paginationCreatorType,
10561101
create() {
10571102
return {
1103+
// calling `this.reducer` assumes we'll be calling the creator as `create.paginationMethods()`
1104+
// if we don't want this assumption, we could use `reducerCreator.create` instead
10581105
prevPage: this.reducer((state: PaginationState) => {
10591106
state.page--
10601107
}),
@@ -1110,7 +1157,7 @@ declare module '@reduxjs/toolkit' {
11101157
ReducerPath extends string,
11111158
> {
11121159
[historyCreatorType]: ReducerCreatorEntry<
1113-
// make sure the creator is only called when state is compatibleState extends HistoryState<unknown>
1160+
// make sure the creator is only called when state is compatible
11141161
State extends HistoryState<any>
11151162
? (this: ReducerCreators<State>) => {
11161163
undo: CaseReducerDefinition<State, PayloadAction>
@@ -1162,18 +1209,22 @@ const historyCreator: ReducerCreator<typeof historyCreatorType> = {
11621209
state.past.push(historyEntry)
11631210
}
11641211
}),
1212+
// highlight-start
1213+
// here we're creating a reducer definition that our `handle` method will be called with
11651214
reset: {
11661215
_reducerDefinitionType: historyCreatorType,
11671216
type: 'reset',
11681217
},
1218+
// highlight-end
11691219
}
11701220
},
11711221
handle(details, definition, context) {
11721222
if (definition.type !== 'reset') {
11731223
throw new Error('Unrecognised definition type: ' + definition.type)
11741224
}
1175-
// use the normal reducer creator to create a case reducer and action creator
11761225
const resetReducer = () => context.getInitialState()
1226+
// you can call other creators' `handle` methods if needed
1227+
// here we're reusing `reducerCreator` to get the expected behaviour of making an action creator for our reducer
11771228
reducerCreator.handle(details, reducerCreator.create(resetReducer), context)
11781229
},
11791230
}
@@ -1250,7 +1301,7 @@ const undoableCreator: ReducerCreator<typeof undoableCreatorType> = {
12501301
reducer: CaseReducer<any, A>,
12511302
): CaseReducer<HistoryState<any>, A> {
12521303
return (state, action) => {
1253-
const [nextState, redoPatch, undoPatch] = produceWithPatches(
1304+
const [nextState, redoPatches, undoPatches] = produceWithPatches(
12541305
state,
12551306
(draft) => {
12561307
const result = reducer(draft.present, action)
@@ -1264,8 +1315,8 @@ const undoableCreator: ReducerCreator<typeof undoableCreatorType> = {
12641315
if (undoable) {
12651316
finalState = createNextState(finalState, (draft) => {
12661317
draft.past.push({
1267-
undo: undoPatch,
1268-
redo: redoPatch,
1318+
undo: undoPatches,
1319+
redo: redoPatches,
12691320
})
12701321
draft.future = []
12711322
})
@@ -1320,3 +1371,55 @@ const postSliceWithHistory = createAppSlice({
13201371
const { undo, redo, reset, updateTitle, togglePinned } =
13211372
postSliceWithHistory.actions
13221373
```
1374+
1375+
:::tip `history-adapter`
1376+
1377+
This example is a somewhat simplified version of the [`history-adapter`](https://www.npmjs.com/package/history-adapter) package, which provides a `createHistoryAdapter` utility that can be used to add undo/redo functionality to a slice.
1378+
1379+
```ts no-transpile
1380+
import {
1381+
createHistoryAdapter,
1382+
historyMethodsCreator,
1383+
undoableCreatorsCreator,
1384+
} from 'history-adapter/redux'
1385+
1386+
const createAppSlice = buildCreateSlice({
1387+
creators: {
1388+
historyMethods: historyMethodsCreator,
1389+
undoableCreators: undoableCreatorsCreator,
1390+
},
1391+
})
1392+
1393+
const postHistoryAdapter = createHistoryAdapter<Post>({ limit: 5 })
1394+
1395+
const postSliceWithHistory = createAppSlice({
1396+
name: 'post',
1397+
initialState: postHistoryAdapter.getInitialState({
1398+
title: '',
1399+
pinned: false,
1400+
}),
1401+
reducers: (create) => {
1402+
const createUndoable = create.undoableCreators(postHistoryAdapter)
1403+
return {
1404+
...create.historyMethods(postHistoryAdapter),
1405+
updateTitle: createUndoable.preparedReducer(
1406+
postHistoryAdapter.withPayload<string>(),
1407+
(state, action) => {
1408+
state.title = action.payload
1409+
},
1410+
),
1411+
togglePinned: createUndoable.preparedReducer(
1412+
postHistoryAdapter.withoutPayload(),
1413+
(state, action) => {
1414+
state.pinned = !state.pinned
1415+
},
1416+
),
1417+
}
1418+
},
1419+
})
1420+
1421+
const { undo, redo, reset, updateTitle, togglePinned } =
1422+
postSliceWithHistory.actions
1423+
```
1424+
1425+
:::

packages/toolkit/src/createSlice.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,8 +1017,8 @@ export function buildCreateSlice<
10171017
'Please use reducer creators passed to callback. Each reducer definition must have a `_reducerDefinitionType` property indicating which handler to use.',
10181018
)
10191019
}
1020-
const handler = handlers[type as RegisteredReducerType]
1021-
if (!handler) {
1020+
const handle = handlers[type]
1021+
if (!handle) {
10221022
throw new Error(`Unsupported reducer type: ${String(type)}`)
10231023
}
10241024
const reducerDetails: ReducerDetails = {
@@ -1027,7 +1027,7 @@ export function buildCreateSlice<
10271027
reducerPath,
10281028
type: getType(name, reducerName),
10291029
}
1030-
handler(
1030+
handle(
10311031
reducerDetails,
10321032
reducerDefinition as any,
10331033
getContext(reducerDetails),
@@ -1184,8 +1184,10 @@ export function buildCreateSlice<
11841184
caseReducers: internalContext.sliceCaseReducersByName as any,
11851185
getInitialState,
11861186
...makeSelectorProps(reducerPath),
1187-
injectInto(injectable, { reducerPath: pathOpt, ...config } = {}) {
1188-
const newReducerPath = pathOpt ?? reducerPath
1187+
injectInto(
1188+
injectable,
1189+
{ reducerPath: newReducerPath = reducerPath, ...config } = {},
1190+
) {
11891191
injectable.inject({ reducerPath: newReducerPath, reducer }, config)
11901192
return {
11911193
...slice,

0 commit comments

Comments
 (0)