Redux: Normalizing the State Shape

Dan Abramov
InstructorDan Abramov
Share this video with your friends

Social Share Links

Send Tweet

We will learn how to normalize the state shape to ensure data consistency that is important in real-world applications.

[00:00] We currently represent the todos in the state free as an array of todo object. However, in the real app, we probably have more than a single array and then todos with the same IDs in different arrays might get out of sync.

[00:15] This is why I wanted to treat my state as a database, and I'm going to keep my todos in an object indexed by the IDs of the todos. I've renamed the reducer to ByID and rather than add a new item at the end or map over every item, now it's going to change the value in the lookup table.

[00:38] Both toggle todo and add todo object is now going to be the same. I want to return a new lookup table where the value unto the ID in the action is going to be the result of calling the reducer on the previous value under reducer ID and the action.

[00:57] This is to reduce a composition but with an object instead of an array. I'm also using the objects spread operator here. It is not a part of ES6 so you need to enable transform-object-rest-spread in babel reducer file, and you need to install babel plugin transform-object-rest-spread for this to work.

[01:18] Anytime the ByID reducer receives an action, it's going to return the copy of its mapping between the IDs and the actual todos with updated todo for the current action. I will let another reducer that keeps track of all the added IDs.

[01:35] We keep the todos themselves in the ByID map now, so the state of this reducer is an array of IDs rather than array of todos. I switch on the action type and the only action I care about is a todo because if I new todo is added, I want to return a new array of IDs with that ID as the last item.

[01:59] For any other actions, I just need to return the current state, that is, the current array of IDs. Finally, I still need to export the single reducer from the todos file, so I'm going to use combined reducers again to combine the ByID and the AllIDs reducers.

[02:19] You can use combined reducers as many times as you like. You don't have to only use it on the top-level reducer. In fact, it's very common that as your app grows, you'll use combine reducers in several places.

[02:32] Now that we have changed the state shape in reducers, we also need to update the selectors that rely on it. The state object then get visible todos is now going to contain ByID and AllIDs fields, because it corresponds to the C of the combined reducer.

[02:49] Since I don't have the array of todos anymore, I will write the selector that is going to calculate it for me. I won't export it because I only plan to use it in the current file and it takes the current state and returns all the todos by mapping AllIDs to the state ByID lookup table.

[03:12] I will use this name selector inside my GetVisible todo selector to obtain an array of todos that I can filter. All todos is an array of todos just like the components I expect so I can return it from the selector and not worry about change in my component code.

[03:31] My todos file has grown quite a bit so it's a good time to extract the todo reducer that manages just when you go todo into a separate file of its own. I created a file called todo in the same folder and I will paste my implementation right there so that I can import it from the todos file.

[03:54] Let's recap how we change those state structures to be normalized and more like a database. I just extracted the todo reducer into a separate file but it hasn't changed in the state.

[04:07] It's still a reducer that handles updates to a single todo item. I'm using this reducer inside the new reducer I wrote today, which is called ByID and its state shape is an object. It reads the ID of the todo to update from the action and it calls the todo reducer with the previous state of this ID and the action.

[04:31] For the addTodo action, the corresponding todo will not exist in the lookup table yet. We're calling the todo reducer with undefined as the first argument. The todo reducer would then return a new todo object when handling addTodo so this object will get assigned under the action ID key inside the next version of the lookup table.

[04:57] Notice how we're mixing the objects spread operator with the computed property syntax, which lets us specify a value at a dynamic key inside action ID. Also, remember that the objects spread operator is experimental and you need a special babel plugin to enable it.

[05:16] I also wrote a second reducer called AllIDs that manages just the array of IDs of the todos. Every time a todo is added, it returns a new array with this ID of the new todo at the very end.

[05:33] It uses the array-spread operator, which is part of ES6, to produce a new array with the AllIDs followed by the new ID. The two reducers now handle the same action. This is fine and very common in the Redux apps.

[05:49] I'm combining the two reducers I wrote into a single reducer by calling combined reducers provided by Redux. You may use combined reducers at multiple levels in your reducer hierarchy.

[06:03] Since the state has changed, I needed to update the selectors that depend on it. I wrote the private selector called get all todos that just assembles all the todos objects from the state by mapping the IDs to the lookup table.

[06:18] For every ID, we get the todo from state.ByID. I'm returning the array of todos with exactly the same shape as the state inside gets visible todos used to be before this change.

[06:33] I can get all todos and now I can use all todos for filtering and I can return it to the new ID that doesn't need to be changed, because all the state shape knowledge is encapsulated in the selector.