We will learn how to normalize the state shape to ensure data consistency that is important in real-world applications.
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?
He has already answered your question clearly at the beginning of this video.
Don't forget to clear the localStorage, when reloading the app with the state change ;)
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
Just read about it here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#Computed_property_names.
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
.
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.
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.
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 withingetVisibleTodos
.
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);
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.
@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