Skip to content

Commit

Permalink
Update createEntityAdapter docs (#479)
Browse files Browse the repository at this point in the history
* Add notes about shallow update behavior, add example for updateOne in usage guide

* Fix typo

* User periods at the end of sentences everywhere
  • Loading branch information
msutkowski committed Apr 5, 2020
1 parent 58aa161 commit ce0aeae
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 20 deletions.
40 changes: 21 additions & 19 deletions docs/api/createEntityAdapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The methods generated by `createEntityAdapter` will all manipulate an "entity st
}
```

`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.
`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.

Sample usage:

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

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.

If not provided, the `state.ids` array will not be sorted, and no guarantees are made about the ordering.
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.

## Return Value

Expand Down Expand Up @@ -195,15 +195,15 @@ export interface EntityAdapter<T> extends EntityStateAdapter<T> {

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:

- `addOne`: accepts a single entity, and adds it
- `addOne`: accepts a single entity, and adds it.
- `addMany`: accepts an array of entities or an object in the shape of `Record<EntityId, T>`, and adds them.
- `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
- `removeOne`: accepts a single entity ID value, and removes the entity with that ID if it exists
- `removeMany`: accepts an array of entity ID values, and removes each entity with those IDs if they exist
- `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
- `updateMany`: accepts an array of update objects, and updates all corresponding entities
- `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.
- `upsertMany`: accepts an array of entities or an object in the shape of `Record<EntityId, T>` that will be upserted.
- `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.
- `removeOne`: accepts a single entity ID value, and removes the entity with that ID if it exists.
- `removeMany`: accepts an array of entity ID values, and removes each entity with those IDs if they exist.
- `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.
- `updateMany`: accepts an array of update objects, and performs shallow updates on all corresponding entities.
- `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.
- `upsertMany`: accepts an array of entities or an object in the shape of `Record<EntityId, T>` that will be shallowly upserted.

Each method has a signature that looks like:

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

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

> **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.
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.

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.
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.

> **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.
### `getInitialState`

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

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:

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

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

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:

- 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
- 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.
- It may also be called with a selector function that accepts the entire Redux state tree and returns the correct entity state object.

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:
Expand Down
13 changes: 12 additions & 1 deletion docs/usage/usage-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,14 @@ import userAPI from './userAPI'
export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
const response = await userAPI.fetchAll()
// In this case, `response.data` would be:
// [{id: 1, first_name: 'Example', last_name: 'User'}]
// [{id: 1, first_name: 'Example', last_name: 'User'}]
return response.data
})

export const updateUser = createAsyncThunk('users/updateOne', async arg => {
const response = await userAPI.updateUser(arg)
// In this case, `response.data` would be:
// { id: 1, first_name: 'Example', last_name: 'UpdatedLastName'}
return response.data
})

Expand All @@ -812,6 +819,10 @@ export const slice = createSlice({
},
extraReducers: builder => {
builder.addCase(fetchUsers.fulfilled, usersAdapter.upsertMany)
builder.addCase(updateUser.fulfilled, (state, { payload }) => {
const { id, ...changes } = payload
usersAdapter.updateOne(state, { id, changes })
})
}
})

Expand Down

0 comments on commit ce0aeae

Please sign in to comment.