Skip to content

Commit f03c2d9

Browse files
committed
Revert "there are no entity methods creators in ba sing se"
This reverts commit 31c4794.
1 parent 31c4794 commit f03c2d9

File tree

9 files changed

+621
-37
lines changed

9 files changed

+621
-37
lines changed

docs/api/createEntityAdapter.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ In other words, they accept a state that looks like `{ids: [], entities: {}}`, a
239239

240240
These CRUD methods may be used in multiple ways:
241241

242-
- They may be passed as case reducers directly to `createReducer` and `createSlice`.
242+
- They may be passed as case reducers directly to `createReducer` and `createSlice`. (also see the [`create.entityMethods`](./createSlice#createentitymethods-entitymethodscreator) slice creator which can assist with this)
243243
- They may be used as "mutating" helper methods when called manually, such as a separate hand-written call to `addOne()` inside of an existing case reducer, if the `state` argument is actually an Immer `Draft` value.
244244
- They may be used as immutable update methods when called manually, if the `state` argument is actually a plain JS object or array.
245245

docs/api/createSlice.mdx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,105 @@ reducers: (create) => {
845845
846846
:::
847847
848+
##### `create.entityMethods` (`entityMethodsCreator`)
849+
850+
Creates a set of reducers for managing a normalized entity state, based on a provided [adapter](./createEntityAdapter).
851+
852+
```ts
853+
import {
854+
createEntityAdapter,
855+
buildCreateSlice,
856+
entityMethodsCreator,
857+
} from '@reduxjs/toolkit'
858+
859+
const createAppSlice = buildCreateSlice({
860+
creators: { entityMethods: entityMethodsCreator },
861+
})
862+
863+
interface Post {
864+
id: string
865+
text: string
866+
}
867+
868+
const postsAdapter = createEntityAdapter<Post>()
869+
870+
const postsSlice = createAppSlice({
871+
name: 'posts',
872+
initialState: postsAdapter.getInitialState(),
873+
reducers: (create) => ({
874+
...create.entityMethods(postsAdapter),
875+
}),
876+
})
877+
878+
export const { setOne, upsertMany, removeAll, ...etc } = postsSlice.actions
879+
```
880+
881+
:::caution
882+
883+
Because this creator returns an object of multiple reducer definitions, it should be spread into the final object returned by the `reducers` callback.
884+
885+
:::
886+
887+
**Parameters**
888+
889+
- **adapter** The [adapter](./createEntityAdapter) to use.
890+
- **config** The configuration object. (optional)
891+
892+
The configuration object can contain some of the following options:
893+
894+
**`selectEntityState`**
895+
896+
A selector to retrieve the entity state from the slice state. Defaults to `state => state`, but should be provided if the entity state is nested.
897+
898+
```ts no-transpile
899+
const postsSlice = createAppSlice({
900+
name: 'posts',
901+
initialState: { posts: postsAdapter.getInitialState() },
902+
reducers: (create) => ({
903+
...create.entityMethods(postsAdapter, {
904+
selectEntityState: (state) => state.posts,
905+
}),
906+
}),
907+
})
908+
```
909+
910+
**`name`, `pluralName`**
911+
912+
It's often desirable to modify the reducer names to be specific to the data type being used. These options allow you to do that.
913+
914+
```ts no-transpile
915+
const postsSlice = createAppSlice({
916+
name: 'posts',
917+
initialState: postsAdapter.getInitialState(),
918+
reducers: (create) => ({
919+
...create.entityMethods(postsAdapter, {
920+
name: 'post',
921+
}),
922+
}),
923+
})
924+
925+
const { addOnePost, upsertManyPosts, removeAllPosts, ...etc } =
926+
postsSlice.actions
927+
```
928+
929+
`pluralName` defaults to `name + 's'`, but can be provided if this isn't desired.
930+
931+
```ts no-transpile
932+
const gooseSlice = createAppSlice({
933+
name: 'geese',
934+
initialState: gooseAdapter.getInitialState(),
935+
reducers: (create) => ({
936+
...create.entityMethods(gooseAdapter, {
937+
name: 'goose',
938+
pluralName: 'geese',
939+
}),
940+
}),
941+
})
942+
943+
const { addOneGoose, upsertManyGeese, removeAllGeese, ...etc } =
944+
gooseSlice.actions
945+
```
946+
848947
### Writing your own creators
849948
850949
In version v2.2.0 (TODO), we introduced a system for including your own creators.

packages/toolkit/src/entities/models.ts

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type { Draft } from 'immer'
33
import type { PayloadAction } from '../createAction'
44
import type { GetSelectorsOptions } from './state_selectors'
55
import type { CastAny, Id as Compute } from '../tsHelpers'
6+
import type { CaseReducerDefinition } from '../createSlice'
7+
import type { CaseReducer } from '../createReducer'
68

79
/**
810
* @public
@@ -158,12 +160,51 @@ export interface EntityStateAdapter<T, Id extends EntityId> {
158160
/**
159161
* @public
160162
*/
161-
export interface EntitySelectors<T, V, Id extends EntityId> {
162-
selectIds: (state: V) => Id[]
163-
selectEntities: (state: V) => Record<Id, T>
164-
selectAll: (state: V) => T[]
165-
selectTotal: (state: V) => number
166-
selectById: (state: V, id: Id) => Compute<UncheckedIndexedAccess<T>>
163+
export type EntitySelectors<
164+
T,
165+
V,
166+
Id extends EntityId,
167+
Single extends string = '',
168+
Plural extends string = DefaultPlural<Single>,
169+
> = Compute<
170+
{
171+
[K in `select${Capitalize<Single>}Ids`]: (state: V) => Id[]
172+
} & {
173+
[K in `select${Capitalize<Single>}Entities`]: (state: V) => Record<Id, T>
174+
} & {
175+
[K in `selectAll${Capitalize<Plural>}`]: (state: V) => T[]
176+
} & {
177+
[K in `selectTotal${Capitalize<Plural>}`]: (state: V) => number
178+
} & {
179+
[K in `select${Capitalize<Single>}ById`]: (
180+
state: V,
181+
id: Id,
182+
) => Compute<UncheckedIndexedAccess<T>>
183+
}
184+
>
185+
186+
export type DefaultPlural<Single extends string> = Single extends ''
187+
? ''
188+
: `${Single}s`
189+
190+
export type EntityReducers<
191+
T,
192+
Id extends EntityId,
193+
State = EntityState<T, Id>,
194+
Single extends string = '',
195+
Plural extends string = DefaultPlural<Single>,
196+
> = {
197+
[K in keyof EntityStateAdapter<
198+
T,
199+
Id
200+
> as `${K}${Capitalize<K extends `${string}One` ? Single : Plural>}`]: EntityStateAdapter<
201+
T,
202+
Id
203+
>[K] extends (state: any) => any
204+
? CaseReducerDefinition<State, PayloadAction>
205+
: EntityStateAdapter<T, Id>[K] extends CaseReducer<any, infer A>
206+
? CaseReducerDefinition<State, A>
207+
: never
167208
}
168209

169210
/**
@@ -187,12 +228,19 @@ export interface EntityAdapter<T, Id extends EntityId>
187228
extends EntityStateAdapter<T, Id>,
188229
EntityStateFactory<T, Id>,
189230
Required<EntityAdapterOptions<T, Id>> {
190-
getSelectors(
231+
getSelectors<
232+
Single extends string = '',
233+
Plural extends string = DefaultPlural<Single>,
234+
>(
191235
selectState?: undefined,
192-
options?: GetSelectorsOptions,
193-
): EntitySelectors<T, EntityState<T, Id>, Id>
194-
getSelectors<V>(
236+
options?: GetSelectorsOptions<Single, Plural>,
237+
): EntitySelectors<T, EntityState<T, Id>, Id, Single, Plural>
238+
getSelectors<
239+
V,
240+
Single extends string = '',
241+
Plural extends string = DefaultPlural<Single>,
242+
>(
195243
selectState: (state: V) => EntityState<T, Id>,
196-
options?: GetSelectorsOptions,
197-
): EntitySelectors<T, V, Id>
244+
options?: GetSelectorsOptions<Single, Plural>,
245+
): EntitySelectors<T, V, Id, Single, Plural>
198246
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import type {
2+
CreatorCaseReducers,
3+
ReducerCreator,
4+
ReducerCreatorEntry,
5+
} from '@reduxjs/toolkit'
6+
import { reducerCreator } from '../createSlice'
7+
import type { WithRequiredProp } from '../tsHelpers'
8+
import type {
9+
Update,
10+
EntityAdapter,
11+
EntityId,
12+
EntityState,
13+
DefaultPlural,
14+
EntityReducers,
15+
} from './models'
16+
import { capitalize } from './utils'
17+
18+
export const entityMethodsCreatorType = /*@__PURE__*/ Symbol()
19+
20+
export interface EntityMethodsCreatorConfig<
21+
T,
22+
Id extends EntityId,
23+
State,
24+
Single extends string,
25+
Plural extends string,
26+
> {
27+
selectEntityState?: (state: State) => EntityState<T, Id>
28+
name?: Single
29+
pluralName?: Plural
30+
}
31+
32+
type EntityMethodsCreator<State> =
33+
State extends EntityState<infer T, infer Id>
34+
? {
35+
<
36+
T,
37+
Id extends EntityId,
38+
Single extends string = '',
39+
Plural extends string = DefaultPlural<Single>,
40+
>(
41+
adapter: EntityAdapter<T, Id>,
42+
config: WithRequiredProp<
43+
EntityMethodsCreatorConfig<T, Id, State, Single, Plural>,
44+
'selectEntityState'
45+
>,
46+
): EntityReducers<T, Id, State, Single, Plural>
47+
<
48+
Single extends string = '',
49+
Plural extends string = DefaultPlural<Single>,
50+
>(
51+
adapter: EntityAdapter<T, Id>,
52+
config?: Omit<
53+
EntityMethodsCreatorConfig<T, Id, State, Single, Plural>,
54+
'selectEntityState'
55+
>,
56+
): EntityReducers<T, Id, State, Single, Plural>
57+
}
58+
: <
59+
T,
60+
Id extends EntityId,
61+
Single extends string = '',
62+
Plural extends string = DefaultPlural<Single>,
63+
>(
64+
adapter: EntityAdapter<T, Id>,
65+
config: WithRequiredProp<
66+
EntityMethodsCreatorConfig<T, Id, State, Single, Plural>,
67+
'selectEntityState'
68+
>,
69+
) => EntityReducers<T, Id, State, Single, Plural>
70+
71+
declare module '@reduxjs/toolkit' {
72+
export interface SliceReducerCreators<
73+
State,
74+
CaseReducers extends CreatorCaseReducers<State>,
75+
Name extends string,
76+
> {
77+
[entityMethodsCreatorType]: ReducerCreatorEntry<EntityMethodsCreator<State>>
78+
}
79+
}
80+
81+
export function createEntityMethods<
82+
T,
83+
Id extends EntityId,
84+
State = EntityState<T, Id>,
85+
Single extends string = '',
86+
Plural extends string = DefaultPlural<Single>,
87+
>(
88+
adapter: EntityAdapter<T, Id>,
89+
{
90+
selectEntityState = (state) => state as unknown as EntityState<T, Id>,
91+
name: nameParam = '' as Single,
92+
pluralName: pluralParam = (nameParam && `${nameParam}s`) as Plural,
93+
}: EntityMethodsCreatorConfig<T, Id, State, Single, Plural> = {},
94+
): EntityReducers<T, Id, State, Single, Plural> {
95+
// template literal computed keys don't keep their type if there's an unresolved generic
96+
// so we cast to some intermediate type to at least check we're using the right variables in the right places
97+
98+
const name = nameParam as 's'
99+
const pluralName = pluralParam as 'p'
100+
const reducer = reducerCreator.create
101+
const reducers: EntityReducers<T, Id, State, 's', 'p'> = {
102+
[`addOne${capitalize(name)}` as const]: reducer<T>((state, action) => {
103+
adapter.addOne(selectEntityState(state), action)
104+
}),
105+
[`addMany${capitalize(pluralName)}` as const]: reducer<
106+
readonly T[] | Record<Id, T>
107+
>((state, action) => {
108+
adapter.addMany(selectEntityState(state), action)
109+
}),
110+
[`setOne${capitalize(name)}` as const]: reducer<T>((state, action) => {
111+
adapter.setOne(selectEntityState(state), action)
112+
}),
113+
[`setMany${capitalize(pluralName)}` as const]: reducer<
114+
readonly T[] | Record<Id, T>
115+
>((state, action) => {
116+
adapter.setMany(selectEntityState(state), action)
117+
}),
118+
[`setAll${capitalize(pluralName)}` as const]: reducer<
119+
readonly T[] | Record<Id, T>
120+
>((state, action) => {
121+
adapter.setAll(selectEntityState(state), action)
122+
}),
123+
[`removeOne${capitalize(name)}` as const]: reducer<Id>((state, action) => {
124+
adapter.removeOne(selectEntityState(state), action)
125+
}),
126+
[`removeMany${capitalize(pluralName)}` as const]: reducer<readonly Id[]>(
127+
(state, action) => {
128+
adapter.removeMany(selectEntityState(state), action)
129+
},
130+
),
131+
[`removeAll${capitalize(pluralName)}` as const]: reducer((state) => {
132+
adapter.removeAll(selectEntityState(state))
133+
}),
134+
[`upsertOne${capitalize(name)}` as const]: reducer<T>((state, action) => {
135+
adapter.upsertOne(selectEntityState(state), action)
136+
}),
137+
[`upsertMany${capitalize(pluralName)}` as const]: reducer<
138+
readonly T[] | Record<Id, T>
139+
>((state, action) => {
140+
adapter.upsertMany(selectEntityState(state), action)
141+
}),
142+
[`updateOne${capitalize(name)}` as const]: reducer<Update<T, Id>>(
143+
(state, action) => {
144+
adapter.updateOne(selectEntityState(state), action)
145+
},
146+
),
147+
[`updateMany${capitalize(pluralName)}` as const]: reducer<
148+
readonly Update<T, Id>[]
149+
>((state, action) => {
150+
adapter.updateMany(selectEntityState(state), action)
151+
}),
152+
}
153+
return reducers as any
154+
}
155+
156+
export const entityMethodsCreator: ReducerCreator<
157+
typeof entityMethodsCreatorType
158+
> = {
159+
type: entityMethodsCreatorType,
160+
create: createEntityMethods,
161+
}

0 commit comments

Comments
 (0)