Redux: Normalizing the State Shape

Dan Abramov
InstructorDan Abramov

Share this video with your friends

Send Tweet
Published 6 years ago
Updated 3 years ago

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.

Jiaming
Jiaming
~ 6 years ago

Thank you Dan! This series of lecture is really helpful! Would you please tell me the advantage of persisting lists of data as Objects rather than as array themselves?

Ningze
Ningze
~ 6 years ago

He has already answered your question clearly at the beginning of this video.

mobility-team
mobility-team
~ 6 years ago

Don't forget to clear the localStorage, when reloading the app with the state change ;)

Manjit Kumar
Manjit Kumar
~ 5 years ago

I am bit confused about the use of array [action.id] as key in state object for a todo instead of plain string action.id, If there is something I may have missed please point out or help me understand why it's like that.

refer- https://github.com/gaearon/todos/blob/11-normalizing-the-state-shape/src/reducers/todos.js#L10

Manjit Kumar
Manjit Kumar
~ 5 years ago

Just read about it here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#Computed_property_names.

yiling
yiling
~ 5 years ago

Since the state.byId already has all the todos, why do we need getAllTodos constant again?

const getAllTodos = (state) =>
  state.allIds.map(id => state.byId[id]);

=========

Looks like I cannot delete a post, I am going to answer my own question here. getAllTodos function transforms a todos object into an array, this array maintains the existing contract within getVisibleTodos.

Eleni Lixourioti
Eleni Lixourioti
~ 4 years ago

This approach of having two reducers, essentially creating two "sub stores", is a bit like de-normalising rather than normalising. I could see why someone would do that as an optimisation, but I can also see how the two sub-states can get out of sync.

For example if you implement DELETE_TODO you can easily forget to add an implementation in the second reducer.

Maybe a different data structure like a Map (easy to get all values, easy to get all keys, easy to check for membership) would suffice. That said, Maps are still second-class citizens, as you cannot have nice things like spreading and you end up converting them to arrays and objects far too often in practice, so maybe this is not ideal either.

J. Matthew
J. Matthew
~ 3 years ago

Thank you Dan! This series of lecture is really helpful! Would you please tell me the advantage of persisting lists of data as Objects rather than as array themselves?

@Ningze The answer at the beginning of the video could be clearer and would benefit greatly from an example. I think Dan is positing that a real-world app might duplicate the same data structure (e.g. a ToDo object) across multiple arrays. For instance, if we stored the generated completed and all arrays in state for some reason (rather than just sending that data to the UI), they could both contain a complete copy of the same ToDo object(s).

That would be a bad way of doing things, because then if we updated the state of a given ToDo, such as to change the value of its text property, we'd have to make that update in both places, or else the two would fall "out of sync," as the video says.

The better way is to store the full data structure in one place, then simply include a reference to it in the other places. Using an object isn't strictly necessary for this—you could just as well store the full ToDo object at a certain index in the all array, and then store only that index in the completed array. You could then do something like this (not that different from what Dan does):
const completedToDos = completedIndices.map(index => allToDoObjects[index]);

However, using an object often serves this purpose better, especially for databases—and @Jiaming this gets at your question—because objects enforce uniqueness better than arrays. It's possible that two ToDos could have the same text and the same completed values, but they should never have the same id. There's nothing built into arrays to stop you from adding two ToDos with the same id, because the array elements are indexed by position in the array, so id is irrelevant. You'd have to write custom code when adding the ToDo to make sure none of the existing array elements had that id. But if you use an object instead, and index each ToDo by its id, then true duplicates are impossible.

Indexing a ToDo by one of its properties—in this case id—also confers performance benefits, because if you know the property value, you can look up the full object instantly, without having to search through an array to find the match. (You do want to be careful in an actual database not to index by any property that could change its value, which is why a UUID or the like is generally preferable.)

When you combine the two techniques of 1) indexing full ToDos (or whatever) by ID and 2) collecting references to subsets of them (like completed and all) in other forms with 3) a third technique of building additional objects that index whatever other ToDo properties to their ID (e.g. look up ToDo ID by text value) you can create a powerful system in which you can find whatever data you need instantly, and you never have to worry about updating it in more than one place*, which is pretty sweet.

*Of course, as @Eleni suggested, you do have to update your various lookups, but there's always a tradeoff.

J. Matthew
J. Matthew
~ 3 years ago

Since the state.byId already has all the todos, why do we need getAllTodos constant again? [...] I am going to answer my own question here. getAllTodos function transforms a todos object into an array, this array maintains the existing contract within getVisibleTodos.

You are right; however, it's worth noting that with the modern Object.values() method (which may not have been available when this course was recorded), we really don't need getAllTodos:

const allTodos = Object.values(state.byId);

J. Matthew
J. Matthew
~ 3 years ago

What I don't get is why this process is called "normalizing" the state shape. I understand the value of the changes made, just not what exactly that term means and why it applies here.

Will Johnson
Will Johnson
~ 3 years ago

@J. Matthew Hi. Thats a good question! From my research normalizing is a term from dealing with data that make its more predictable to deal with and easier to access. There are more in depth explanations on the egghead community forum.

https://community.egghead.io/t/normalizing-state-shape-in-redux/624