You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* Add basic createEntityAdapter usage docs
* Fix by hand example code
* Apply suggestions from code review
Co-Authored-By: Mark Erikson <mark@isquaredsoftware.com>
* Copy updates per feedback
* Fix normalizr example and have it align with the normalizr usage demo
* Show combination of normalizr and createEntityAdapter
* Update docs and ts usage example for passing non-arrays on many methods
* Remove redundant comment and add to section intro
* Update createEntityAdapter API docs
* Update codesandbox links
* Assorted usage guide tweaks
* Netlify tweaks, 1
* Netlify tweaks, 2
* Netlify tweaks, 3
* Fix headers and tweak phrasing
* Shorten line lengths
Co-authored-by: Mark Erikson <mark@isquaredsoftware.com>
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:
197
197
198
198
-`addOne`: accepts a single entity, and adds it
199
-
-`addMany`: accepts an array of entities, and adds them
200
-
-`setAll`: accepts an array of entities, and replaces the existing entity contents with the values in the array
199
+
-`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
201
-`removeOne`: accepts a single entity ID value, and removes the entity with that ID if it exists
202
202
-`removeMany`: accepts an array of entity ID values, and removes each entity with those IDs if they exist
203
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
204
-`updateMany`: accepts an array of update objects, and updates all corresponding entities
205
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 that will be upserted.
206
+
-`upsertMany`: accepts an array of entities or an object in the shape of `Record<EntityId, T>`that will be upserted.
Copy file name to clipboardExpand all lines: docs/usage/usage-guide.md
+310Lines changed: 310 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -700,3 +700,313 @@ interface ThunkAPI {
700
700
```
701
701
702
702
You can use any of these as needed inside the payload callback to determine what the final result should be.
703
+
704
+
## Managing Normalized Data
705
+
706
+
Most applications typically deal with data that is deeply nested or relational. The goal of normalizing data is to efficiently organize the data in your state. This is typically done by storing collections as objects with the key of an `id`, while storing a sorted array of those `ids`. For a more in-depth explanation and further examples, there is a great reference in the [Redux docs page on "Normalizing State Shape"](https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape).
707
+
708
+
### Normalizing by hand
709
+
710
+
Normalizing data doesn't require any special libraries. Here's a basic example of how you might normalize the response from a `fetchAll` API request that returns data in the shape of `{ users: [{id: 1, first_name: 'normalized', last_name: 'person'}] }`, using some hand-written logic:
Although we're capable of writing this code, it does become repetitive, especially if you're handling multiple types of data. In addition, this example only handles loading entries into the state, not updating them.
743
+
744
+
### Normalizing with `normalizr`
745
+
746
+
[`normalizr`](https://github.com/paularmstrong/normalizr) is a popular existing library for normalizing data. You can use it on its own without Redux, but it is very commonly used with Redux. The typical usage is to format collections from an API response and then process them in your reducers.
As with the hand-written version, this doesn't handle adding additional entries into the state, or updating them later - it's just loading in everything that was received.
780
+
781
+
### Normalizing with `createEntityAdapter`
782
+
783
+
Redux Toolkit's `createEntityAdapter` API provides a standardized way to store your data in a slice by taking a collection and putting it into the shape of `{ ids: [], entities: {} }`. Along with this predefined state shape, it generates a set of reducer functions and selectors that know how to work with the data.
You can [view the full code of this example usage on CodeSandbox](https://codesandbox.io/s/rtk-entities-basic-example-1xubt)
825
+
826
+
### Using `createEntityAdapter` with Normalization Libraries
827
+
828
+
If you're already using `normalizr` or another normalization library, you could consider using it along with `createEntityAdapter`. To expand on the examples above, here is a demonstration of how we could use `normalizr` to format a payload, then leverage the utilities `createEntityAdapter` provides.
829
+
830
+
By default, the `setAll`, `addMany`, and `upsertMany` CRUD methods expect an array of entities. However, they also allow you to pass in an object that is in the shape of `{ 1: { id: 1, ... }}` as an alternative, which makes it easier to insert pre-normalized data.
You can [view the full code of this example `normalizr` usage on CodeSandbox](https://codesandbox.io/s/rtk-entities-basic-example-with-normalizr-bm3ie)
927
+
928
+
### Using selectors with `createEntityAdapter`
929
+
930
+
The entity adapter providers a selector factory that generates the most common selectors for you. Taking the examples above, we can add selectors to our `usersSlice` like this:
931
+
932
+
```js
933
+
// Rename the exports for readability in component usage
934
+
exportconst {
935
+
selectById:selectUserById,
936
+
selectIds:selectUserIds,
937
+
selectEntities:selectUserEntities,
938
+
selectAll:selectAllUsers,
939
+
selectTotal:selectTotalUsers
940
+
} =usersAdapter.getSelectors(state=>state.users)
941
+
```
942
+
943
+
You could then use these selectors in a component like this:
By default, `createEntityAdapter` assumes that your data has unique IDs in an `entity.id` field. If your data set stores its ID in a different field, you can pass in a `selectId` argument that returns the appropriate field.
975
+
976
+
```js
977
+
// In this instance, our user data always has a primary key of `idx`
978
+
constuserData= {
979
+
users: [
980
+
{ idx:1, first_name:'Test' },
981
+
{ idx:2, first_name:'Two' }
982
+
]
983
+
}
984
+
985
+
// Since our primary key is `idx` and not `id`,
986
+
// pass in an ID selector to return that field instead
987
+
exportconstusersAdapter=createEntityAdapter({
988
+
selectId:user=>user.idx
989
+
})
990
+
```
991
+
992
+
### Sorting Entities
993
+
994
+
`createEntityAdapter` provides a `sortComparer` argument that you can leverage to sort the collection of `ids` in state. This can be very useful for when you want to guarantee a sort order and your data doesn't come presorted.
995
+
996
+
```js
997
+
// In this instance, our user data always has a primary key of `idx`
998
+
constuserData= {
999
+
users: [
1000
+
{ id:1, first_name:'Test' },
1001
+
{ id:2, first_name:'Banana' }
1002
+
]
1003
+
}
1004
+
1005
+
// Sort by `first_name`. `state.ids` would be ordered as
1006
+
// `ids: [ 2, 1 ]`, since 'B' comes before 'T'.
1007
+
// When using the provided `selectAll` selector, the result would be sorted:
When using a library like [`normalizr`](https://github.com/paularmstrong/normalizr/), your normalized data will resemble this shape:
576
+
577
+
```js
578
+
{
579
+
result:1,
580
+
entities: {
581
+
1: { id:1, other:'property' },
582
+
2: { id:2, other:'property' }
583
+
}
584
+
}
585
+
```
586
+
587
+
The methods `addMany`, `upsertMany`, and `setAll` all allow you to pass in the `entities` portion of this directly with no extra conversion steps. However, the `normalizr` TS typings currently do not correctly reflect that multiple data types may be included in the results, so you will need to specify that type structure yourself.
588
+
589
+
Here is an example of how that would look:
590
+
591
+
```ts
592
+
typeAuthor= { id:number; name:string }
593
+
typeArticle= { id:number; title:string }
594
+
typeComment= { id:number; commenter:number }
595
+
596
+
exportconst fetchArticle =createAsyncThunk(
597
+
'articles/fetchArticle',
598
+
async (id:number) => {
599
+
const data =awaitfakeAPI.articles.show(id)
600
+
// Normalize the data so reducers can responded to a predictable payload.
601
+
// Note: at the time of writing, normalizr does not automatically infer the result,
602
+
// so we explicitly declare the shape of the returned normalized data as a generic arg.
// The type signature on action.payload matches what we passed into the generic for `normalize`, allowing us to access specific properties on `payload.articles` if desired
0 commit comments