Skip to content

Commit ce0aeae

Browse files
authored
Update createEntityAdapter docs (#479)
* Add notes about shallow update behavior, add example for updateOne in usage guide * Fix typo * User periods at the end of sentences everywhere
1 parent 58aa161 commit ce0aeae

File tree

2 files changed

+33
-20
lines changed

2 files changed

+33
-20
lines changed

docs/api/createEntityAdapter.md

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ The methods generated by `createEntityAdapter` will all manipulate an "entity st
3131
}
3232
```
3333

34-
`createEntityAdapter` may be called multiple times in an application. If you are using it with plain JavaScript, you may be able to reuse a single adapter definition with multiple entity types if they're similar enough (such as all having an `entity.id` field). For TypeScript usage, you will need to call `createEntityAdapter` a separate time for each distinct `Entity` type, so that the type definitions are inferred correctly.
34+
`createEntityAdapter` may be called multiple times in an application. If you are using it with plain JavaScript, you may be able to reuse a single adapter definition with multiple entity types if they're similar enough (such as all having an `entity.id` field). For [TypeScript usage](../usage/usage-with-typescript.md#createentityadapter), you will need to call `createEntityAdapter` a separate time for each distinct `Entity` type, so that the type definitions are inferred correctly.
3535

3636
Sample usage:
3737

@@ -93,7 +93,7 @@ A callback function that accepts two `Entity` instances, and should return a sta
9393

9494
If provided, the `state.ids` array will be kept in sorted order based on comparisons of the entity objects, so that mapping over the IDs array to retrieve entities by ID should result in a sorted array of entities.
9595

96-
If not provided, the `state.ids` array will not be sorted, and no guarantees are made about the ordering.
96+
If not provided, the `state.ids` array will not be sorted, and no guarantees are made about the ordering. In other words, `state.ids` can be expected to behave like a standard Javascript array.
9797

9898
## Return Value
9999

@@ -195,15 +195,15 @@ export interface EntityAdapter<T> extends EntityStateAdapter<T> {
195195

196196
The primary content of an entity adapter is a set of generated reducer functions for adding, updating, and removing entity instances from an entity state object:
197197

198-
- `addOne`: accepts a single entity, and adds it
198+
- `addOne`: accepts a single entity, and adds it.
199199
- `addMany`: accepts an array of entities or an object in the shape of `Record<EntityId, T>`, and adds them.
200-
- `setAll`: accepts an array of entities or an object in the shape of `Record<EntityId, T>`, and replaces the existing entity contents with the values in the array
201-
- `removeOne`: accepts a single entity ID value, and removes the entity with that ID if it exists
202-
- `removeMany`: accepts an array of entity ID values, and removes each entity with those IDs if they exist
203-
- `updateOne`: accepts an "update object" containing an entity ID and an object containing one or more new field values to update inside a `changes` field, and updates the corresponding entity
204-
- `updateMany`: accepts an array of update objects, and updates all corresponding entities
205-
- `upsertOne`: accepts a single entity. If an entity with that ID exists, the fields in the update will be merged into the existing entity, with any matching fields overwriting the existing values. If the entity does not exist, it will be added.
206-
- `upsertMany`: accepts an array of entities or an object in the shape of `Record<EntityId, T>` that will be upserted.
200+
- `setAll`: accepts an array of entities or an object in the shape of `Record<EntityId, T>`, and replaces the existing entity contents with the values in the array.
201+
- `removeOne`: accepts a single entity ID value, and removes the entity with that ID if it exists.
202+
- `removeMany`: accepts an array of entity ID values, and removes each entity with those IDs if they exist.
203+
- `updateOne`: accepts an "update object" containing an entity ID and an object containing one or more new field values to update inside a `changes` field, and performs a shallow update on the corresponding entity.
204+
- `updateMany`: accepts an array of update objects, and performs shallow updates on all corresponding entities.
205+
- `upsertOne`: accepts a single entity. If an entity with that ID exists, it will perform a shallow update and the specified fields will be merged into the existing entity, with any matching fields overwriting the existing values. If the entity does not exist, it will be added.
206+
- `upsertMany`: accepts an array of entities or an object in the shape of `Record<EntityId, T>` that will be shallowly upserted.
207207

208208
Each method has a signature that looks like:
209209

@@ -216,14 +216,16 @@ In other words, they accept a state that looks like `{ids: [], entities: {}}`, a
216216
These CRUD methods may be used in multiple ways:
217217

218218
- They may be passed as case reducers directly to `createReducer` and `createSlice`.
219-
- 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
220-
- They may be used as immutable update methods when called manually, if the `state` argument is actually a plain JS object or array
219+
- 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.
220+
- They may be used as immutable update methods when called manually, if the `state` argument is actually a plain JS object or array.
221221

222222
> **Note**: These methods do _not_ have corresponding Redux actions created - they are just standalone reducers / update logic. **It is entirely up to you to decide where and how to use these methods!** Most of the time, you will want to pass them to `createSlice` or use them inside another reducer.
223223
224224
Each method will check to see if the `state` argument is an Immer `Draft` or not. If it is a draft, the method will assume that it's safe to continue mutating that draft further. If it is not a draft, the method will pass the plain JS value to Immer's `createNextState()`, and return the immutably updated result value.
225225

226-
The `argument` may be either a plain value (such as a single `Entity` object for `addOne()` or an `Entity[]` array for `addMany()`), or a `PayloadAction` action object with that same value as`action.payload`. This enables using them as both helper functions and reducers.
226+
The `argument` may be either a plain value (such as a single `Entity` object for `addOne()` or an `Entity[]` array for `addMany()`, or a `PayloadAction` action object with that same value as `action.payload`. This enables using them as both helper functions and reducers.
227+
228+
> **Note on shallow updates:** `updateOne`, `updateMany`, `upsertOne`, and `upsertMany` only perform shallow updates in a mutable manner. This means that if your update/upsert consists of an object that includes nested properties, the value of the incoming change will overwrite the **entire** existing nested object. This may be unintended behavior for your application. As a general rule, these methods are best used with [normalized data](../usage/usage-guide.md#managing-normalized-data) that _do not_ have nested properties.
227229
228230
### `getInitialState`
229231

@@ -250,17 +252,17 @@ const booksSlice = createSlice({
250252

251253
The entity adapter will contain a `getSelectors()` function that returns a set of selectors that know how to read the contents of an entity state object:
252254

253-
- `selectIds`: returns the `state.ids` array
254-
- `selectEntities`: returns the `state.entities` lookup table
255-
- `selectAll`: maps over the `state.ids` array, and returns an array of entities in the same order
256-
- `selectTotal`: returns the total number of entities being stored in this state
257-
- `selectById`: given the state and an entity ID, returns the entity with that ID or `undefined`
255+
- `selectIds`: returns the `state.ids` array.
256+
- `selectEntities`: returns the `state.entities` lookup table.
257+
- `selectAll`: maps over the `state.ids` array, and returns an array of entities in the same order.
258+
- `selectTotal`: returns the total number of entities being stored in this state.
259+
- `selectById`: given the state and an entity ID, returns the entity with that ID or `undefined`.
258260

259261
Each selector function will be created using the `createSelector` function from Reselect, to enable memoizing calculation of the results.
260262

261263
Because selector functions are dependent on knowing where in the state tree this specific entity state object is kept, `getSelectors()` can be called in two ways:
262264

263-
- If called without any arguments, it returns an "unglobalized" set of selector functions that assume their `state` argument is the actual entity state object to read from
265+
- If called without any arguments, it returns an "unglobalized" set of selector functions that assume their `state` argument is the actual entity state object to read from.
264266
- It may also be called with a selector function that accepts the entire Redux state tree and returns the correct entity state object.
265267

266268
For example, the entity state for a `Book` type might be kept in the Redux state tree as `state.books`. You can use `getSelectors()` to read from that state in two ways:

docs/usage/usage-guide.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,14 @@ import userAPI from './userAPI'
793793
export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
794794
const response = await userAPI.fetchAll()
795795
// In this case, `response.data` would be:
796-
// [{id: 1, first_name: 'Example', last_name: 'User'}]
796+
// [{id: 1, first_name: 'Example', last_name: 'User'}]
797+
return response.data
798+
})
799+
800+
export const updateUser = createAsyncThunk('users/updateOne', async arg => {
801+
const response = await userAPI.updateUser(arg)
802+
// In this case, `response.data` would be:
803+
// { id: 1, first_name: 'Example', last_name: 'UpdatedLastName'}
797804
return response.data
798805
})
799806

@@ -812,6 +819,10 @@ export const slice = createSlice({
812819
},
813820
extraReducers: builder => {
814821
builder.addCase(fetchUsers.fulfilled, usersAdapter.upsertMany)
822+
builder.addCase(updateUser.fulfilled, (state, { payload }) => {
823+
const { id, ...changes } = payload
824+
usersAdapter.updateOne(state, { id, changes })
825+
})
815826
}
816827
})
817828

0 commit comments

Comments
 (0)