As mentioned in Normalizing State Shape, the Normalizr library is frequently used to transform nested response data into a normalized shape suitable for integration into the store. However, that doesn't address the issue of executing further updates to that normalized data as it's being used elsewhere in the application. There are a variety of different approaches that you can use, based on your own preference. We'll use the example of handling mutations for Comments on a Post.
One approach is to merge the contents of the action into the existing state. In this case, we can use deep recursive merge, not just a shallow copy, to allow for actions with partial items to update stored items. The Lodash
merge function can handle this for us:
This requires the least amount of work on the reducer side, but does require that the action creator potentially do a fair amount of work to organize the data into the correct shape before the action is dispatched. It also doesn't handle trying to delete an item.
If we have a nested tree of slice reducers, each slice reducer will need to know how to respond to this action appropriately. We will need to include all the relevant data in the action. We need to update the correct Post object with the comment's ID, create a new Comment object using that ID as a key, and include the Comment's ID in the list of all Comment IDs. Here's how the pieces for this might fit together:
The example is a bit long, because it's showing how all the different slice reducers and case reducers fit together. Note the delegation involved here. The
postsById slice reducer delegates the work for this case to
addComment, which inserts the new Comment's ID into the correct Post item. Meanwhile, both the
allComments slice reducers have their own case reducers, which update the Comments lookup table and list of all Comment IDs appropriately.
Since reducers are just functions, there's an infinite number of ways to split up this logic. While using slice reducers is the most common, it's also possible to organize behavior in a more task-oriented structure. Because this will often involve more nested updates, you may want to use an immutable update utility library like dot-prop-immutable or object-path-immutable to simplify the update statements. Here's an example of what that might look like:
This approach makes it very clear what's happening for the
"ADD_COMMENTS" case, but it does require nested updating logic, and some specific knowledge of the state tree shape. Depending on how you want to compose your reducer logic, this may or may not be desired.
The Redux-ORM library provides a very useful abstraction layer for managing normalized data in a Redux store. It allows you to declare Model classes and define relations between them. It can then generate the empty "tables" for your data types, act as a specialized selector tool for looking up the data, and perform immutable updates on that data.
There's a couple ways Redux-ORM can be used to perform updates. First, the Redux-ORM docs suggest defining reducer functions on each Model subclass, then including the auto-generated combined reducer function into your store:
The Redux-ORM library maintains relationships between models for you. Updates are by default applied immutably, simplifying the update process.
Another variation on this is to use Redux-ORM as an abstraction layer within a single case reducer:
By using the session interface you can now use relationship accessors to directly access referenced models:
Overall, Redux-ORM provides a very useful set of abstractions for defining relations between data types, creating the "tables" in our state, retrieving and denormalizing relational data, and applying immutable updates to relational data.