Managing Normalized Data

    One approach is to merge the contents of the action into the existing state. In this case, we need to do a deep recursive merge, not just a shallow copy. The Lodash 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:

    1. // actions.js
    2. function addComment(postId, commentText) {
    3. // Generate a unique ID for this comment
    4. const commentId = generateId('comment')
    5. return {
    6. type: 'ADD_COMMENT',
    7. payload: {
    8. postId,
    9. commentId,
    10. commentText
    11. }
    12. }
    13. }
    14. // reducers/posts.js
    15. function addComment(state, action) {
    16. const { payload } = action
    17. const { postId, commentId } = payload
    18. // Look up the correct post, to simplify the rest of the code
    19. const post = state[postId]
    20. return {
    21. ...state,
    22. // Update our Post object with a new "comments" array
    23. [postId]: {
    24. ...post,
    25. comments: post.comments.concat(commentId)
    26. }
    27. }
    28. }
    29. function postsById(state = {}, action) {
    30. switch (action.type) {
    31. case 'ADD_COMMENT':
    32. return addComment(state, action)
    33. default:
    34. return state
    35. }
    36. }
    37. function allPosts(state = [], action) {
    38. // omitted - no work to be done for this example
    39. }
    40. const postsReducer = combineReducers({
    41. byId: postsById,
    42. allIds: allPosts
    43. })
    44. // reducers/comments.js
    45. function addCommentEntry(state, action) {
    46. const { payload } = action
    47. const { commentId, commentText } = payload
    48. // Create our new Comment object
    49. const comment = { id: commentId, text: commentText }
    50. // Insert the new Comment object into the updated lookup table
    51. return {
    52. [commentId]: comment
    53. }
    54. }
    55. function commentsById(state = {}, action) {
    56. switch (action.type) {
    57. case 'ADD_COMMENT':
    58. return addCommentEntry(state, action)
    59. default:
    60. return state
    61. }
    62. }
    63. function addCommentId(state, action) {
    64. const { payload } = action
    65. const { commentId } = payload
    66. // Just append the new Comment's ID to the list of all IDs
    67. return state.concat(commentId)
    68. }
    69. function allComments(state = [], action) {
    70. switch (action.type) {
    71. case 'ADD_COMMENT':
    72. return addCommentId(state, action)
    73. default:
    74. return state
    75. }
    76. }
    77. const commentsReducer = combineReducers({
    78. byId: commentsById,
    79. allIds: allComments
    80. })

    Other Approaches

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

    1. // models.js
    2. import { Model, fk, attr, ORM } from 'redux-orm'
    3. export class Post extends Model {
    4. static get fields() {
    5. return {
    6. id: attr(),
    7. name: attr()
    8. }
    9. }
    10. static reducer(action, Post, session) {
    11. switch (action.type) {
    12. case 'CREATE_POST': {
    13. Post.create(action.payload)
    14. break
    15. }
    16. }
    17. }
    18. }
    19. Post.modelName = 'Post'
    20. return {
    21. id: attr(),
    22. text: attr(),
    23. // Define a foreign key relation - one Post can have many Comments
    24. postId: fk({
    25. to: 'Post', // must be the same as Post.modelName
    26. as: 'post', // name for accessor (comment.post)
    27. relatedName: 'comments' // name for backward accessor (post.comments)
    28. })
    29. }
    30. }
    31. static reducer(action, Comment, session) {
    32. switch (action.type) {
    33. case 'ADD_COMMENT': {
    34. Comment.create(action.payload)
    35. break
    36. }
    37. }
    38. }
    39. }
    40. Comment.modelName = 'Comment'
    41. // Create an ORM instance and hook up the Post and Comment models
    42. export const orm = new ORM()
    43. orm.register(Post, Comment)
    44. // main.js
    45. import { createStore, combineReducers } from 'redux'
    46. import { createReducer } from 'redux-orm'
    47. import { orm } from './models'
    48. const rootReducer = combineReducers({
    49. // Insert the auto-generated Redux-ORM reducer. This will
    50. // initialize our model "tables", and hook up the reducer
    51. // logic we defined on each Model subclass
    52. entities: createReducer(orm)
    53. })
    54. // Dispatch an action to create a Post instance
    55. store.dispatch({
    56. type: 'CREATE_POST',
    57. payload: {
    58. id: 1,
    59. name: 'Test Post Please Ignore'
    60. }
    61. })
    62. // Dispatch an action to create a Comment instance as a child of that Post
    63. store.dispatch({
    64. type: 'ADD_COMMENT',
    65. payload: {
    66. id: 123,
    67. text: 'This is a comment',
    68. postId: 1
    69. }
    70. })

    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:

    1. const session = orm.session(store.getState().entities)
    2. const comment = session.Comment.first() // Comment instance
    3. const { post } = comment // Post instance