Lesson Description

The "Data Normalization Exercise" Lesson is part of the full, State Management at Scale in React & Next.js course featured in this preview video. Here's what you'd learn in this lesson:

David instructs students to normalize nested data in a Redux application, focusing on simplifying data structures for easier state management. He also walks through a possible solution of normalizing nested data by moving from nested arrays to a flatter structure, emphasizing the benefits of normalization for efficient state updates and management.

Preview
Close

Transcript from the "Data Normalization Exercise" Lesson

[00:00:00]
>> David Khourshid: Let's jump into the exercise. So this is actually going to be a pretty fun exercise. We have a different part of the application here. Oops. There we go. So if you go to the normalization exercise now, you're going to see somewhere where a user can enter their travel itinerary.

[00:00:23]
And so we're using a reducer for this. You could add a destination. So let's say that you're going to Osaka. You want to, I don't know, eat sushi and see the ocean, I don't know. And then let's say that you have another destination like Tokyo, where you want to visit team labs and eat more sushi, whatever there is to do in Japan.

[00:00:54]
And this is nested data. So you have this, you have destinations, and so we have multiple. And then you have to do items in each of those destinations for the travel itinerary. But what we really want to do is simplify how all of this works so that we don't have to go deeply into the destination and then find the to do item so that we could do things like delete it and things like that.

[00:01:22]
So looking back at the page, we see that we have a destination within the array of todo items. And then we have our to do items over there. And our goal is to simply normalize that data so that it's not nested. And if you want some fun state challenges too, then you could do things like focusing the destination input.

[00:01:46]
So, for example, if I add a new destination, well, it already works like that, but you could work on, for example, if you say something and then you press add, it should focus back on that field. It's a fun exercise. And also we're going to be taking a look as a bonus at implementing undo and redo and just seeing how that works, because I think that that is a very advanced but important state management skill to learn.

[00:02:22]
>> David Khourshid: All right, let's normalize some data. So again, we have to do item being nested inside of destinations under this TO DOS array. And we have all of our destinations in here. So I'm going to be like a broken record. We're doing things side by side without. Without deleting code first.

[00:02:41]
So we are going to put our TODOS in to do item. And we're also going to be adding another bit of information. We're going to be giving a relation for what destination it belongs to. Now, immediately you're going to see some red, which is great because the red really helps us out and tells us you updated this.

[00:03:04]
So now you need to, you know, make sure that everything is updated. So let's say const. New to do. Oops, not you new TODOS. And we're going to give it that random id the text, but we're also going to give it the action.destination ID, which we already have.

[00:03:26]
So we could pass in that new TODO here just to make typescript happy. But what we're also going to do is we're going to add this to the TODOS array. So we're going to just push it here. By the way, I really like doing state TODOS. Concat. It's up to you.

[00:03:45]
You could do that. You could do the spread operator. I don't think it really matters. All right, and then there is one more red part over here where we need to add that array of TODOS. So are we done? Not exactly yet. So we have almost normalized our data, but we still have this array of TODOS.

[00:04:11]
So we'll go ahead and comments that out and then we could just remove that. Let's see, there's some other red over here. So this one, we actually don't need this anymore. And this is one of the main points of this, where we don't really need to keep around this boilerplate code for going deeply into a structure, because, yeah, we could just delete that code.

[00:04:44]
So same thing over here, we're going to filter the TODOS and this becomes a less expensive operation. You could do even further normalization by using an object instead of an array, where the object keys are the IDs. So this is a form of normalization, but again, you could go further, and it's actually much faster if you use an object technically, because it's an O of one operation versus an O of N operation.

[00:05:14]
But just to make things simple, I like keeping it as an array and because that also preserves the order of the items too. So that's one of the trade offs that you have to think of when you're deciding to normalize via an object or an array. And there's also one more hidden thing over here, which is this.

[00:05:33]
When you delete a destination, now you also have to filter through the TODOS and you have to make sure that any TODOS that are related to that destination are gone as well. So I mean, they're still going to be invisible because you're deleting the destination so they don't show up in the ui.

[00:05:53]
But it's just nice to do. And especially if you're syncing with a server, you don't want to have those impossible states of orphans to do items that are trying to relate to a destination that doesn't exist. All right, so now this code is going to become a lot simpler.

[00:06:12]
We have our destinations over here that come from the state. So now we could just read the to dos and actually, we might need to do something a little bit different here. So we split screen this. We need to split this out into its own components. And here's the way I like to do this.

[00:06:39]
It's actually pretty common in React apps, especially big ones, where it's like, okay, I'm just going to map through values, and then I'm going to use this convenient inline function to just put everything in there. And then you realize that that gets pretty big, and then you have to use a hook for something.

[00:06:54]
So then it becomes a little bit annoying, the fact that you inlined all of this stuff. And so, that's why I do recommend just creating individual components whenever it's feasible. But, sometimes it's not. And we could just create a. We could refactor. Anyway, so let's make a function destination and we're going to have the destination, and then we're just going to return everything inside that card, unless it doesn't let us.

[00:07:26]
There we go. And then we're going to see a bunch of red. [COUGH] So this is another important thing to talk about. When we had it inline, we already had dispatch and things like that. So there's a couple of different approaches for solving this. We could either pass dispatch directly in here if we wanted to, or the other approaches, we could abstract that away and make this component truly standalone.

[00:07:57]
So we could say, for example. Onupdate, destination, etc. For simplicity, I'm just going to pass in dispatch here, but you could do whatever approach works best for you. So this dispatch we are going to have action void and then most everything should work fine. So we will remove that for now.

[00:08:32]
Now we also need to grab the TODOS, so we could pass that in through here and then this is going to be just our array of TODOS that are related to that destination. So now instead of destination todos we would have TODOS length. Instead of destination TODOS map, we would just map over the TODOS and so now we can take this card and we could replace it with our destination, which is in here.

[00:09:11]
And then the TODOS are going to be the TODOS where the todo destination ID matches the destination ID. Still getting an error here. So let's figure out what's going, we need a key. Of course. Another thing too, always prefer to have a unique ID when it comes to mapping over something.

[00:09:38]
I know we've all done the habit where we use indexes and things like that, but the most reliable thing is going to be a key. And so the reason why is that things can change order and then the UI states. So the natural DOM state of whatever component is being worked on is going to be completely lost.

[00:10:01]
So imagine you have, for example, a list of TODOS, each of which has an input or something, and you're typing somewhere and then another TODOS is prepended to the list. Now, because that index is different, React treats that TODOS item as a completely different component and so you're going to lose focus.

[00:10:20]
So that's why if you have the ID, that won't happen because that ID is going to stay exactly the same. So that's another mini lesson there. All right, so everything mostly looks good. We've normalized our data. We no longer need those to. There's another small thing that I want to talk about.

[00:10:43]
In our TODO item, when we have like a destination ID, like for example, it's just a string over here and we're just creating a TODO, and we're passing in the destination ID, absolutely nothing. And this is really interesting, nothing is stopping us from doing this or even just doing like a TODO ID or whatever.

[00:11:07]
So we could put anything invalid in there. Are there any guardrails that can help us just sort of prevent this anti pattern or not really an anti pattern, but just a potential for errors to happen? The answer is yes, and it's by. This is sort of an advanced typescript technique.

[00:11:28]
It's called a branded type. So it looks like this type brand B and we're gonna do a simple version where it's just a string and we have this weird little object over here. Basically this creates a special type that says it is a string, but it is a special kind of string.

[00:11:50]
So just to give you an idea, we might have a destination ID and we could give it the brand of destination ID and we can also have a TODO ID which we could give the brand of TODO ID. Functionally, these are strings. They're going to work just like strings.

[00:12:06]
And as far as the runtime is concerned, they are strings. We're not adding any extra properties to it or anything like that. But here's the key, when we put this in there and we put that branded type, we immediately see red because it's like, hey, we can't actually guarantee that this action destination ID is really a destination ID.

[00:12:31]
So now we have to enforce that upstream. So we could say this should be a destination ID and now we're going to get read somewhere else. And so destination ID, if we go to destination, we could say every destination. Wait, that's destination. Let's call this destination card. Okay, so destination where was, nope, this is what happens when I use the same name for things.

[00:13:14]
But anyway, so in the destination, we could say that whatever ID is in here is definitely a destination ID and whatever ID is here is definitely a TODO ID. And so when we're actually creating that, we can coerce, for example, this random UUID. We could say this, just assume that it is a destination ID because we're creating it from scratch, so we have full control over there.

[00:13:42]
So same thing over here, we could coerce this to be a todo ID and so now when we're here. All right, so now let's try doing something bad. Let's say we're updating the destination and we do some junk over here. It's not gonna yell at us because that's still a string.

[00:14:08]
And so we need to change it in a couple of other places. But now we're going to see red and it's going to be because we are not using the correct branded type. So, oops, so we could change that and see that it's fixed. So we have destination ID and now everything is fine.

[00:14:33]
So, when we create a todo so over here, we can be sure that we're not just putting in any random destination ID, but we're actually putting in an actual destination ID. So that's, yeah, any questions on anything I showed you there so far?
>> Speaker 2: Curious of so I've seen something similar to the branded type, and I've seen people like really lean into enums to try and avoid using strings anywhere in the application.

[00:15:20]
So like for instance, on your types, rather than using string add to do, you could just do like an enumerate and then have like a set of different actions, would you say there is any drawback or benefits to between the two?
>> David Khourshid: I think that there's plus and minuses to both approaches.

[00:15:45]
Personally, when I'm working on an application, I will first start off with just the strings, just because I know that if I make a typo here it's it's going to yell at me anyway. And also, I do still get autocompletes when I do. Do that. The downside though, for that, and this is one of the benefits of using enums, is that, you see, I'm trying to rename this element and it's saying, I cannot rename this element, at least not at the moment.

[00:16:12]
Just because a string is a string, just because it happens to be in an object, doesn't mean that we could replace all usages of the string with whatever we decide to put there. But if we use const enumeration, action type, and then we have something like this, and then we replace this with action type, dot, add destination, then you know that then we could actually change that, I believe.

[00:16:40]
No, you can't. But I mean, you could change it here. And also if you change this, it doesn't matter because that value is going to be the same. And so that's why you're going to see red there and it's going to be like, hey, you should probably just do this.

[00:16:57]
So I do see the benefits of it. I would say Enums and TypeScript are a little bit messy. If you are going to use enums just because of the way they implemented it. If you are, then I strongly recommend using const enums just because I believe, if I'm not mistaken, that they're just the runtime values are just the strings.

[00:17:20]
It's not like some crazy enum object. It's just going to be a string.
>> Speaker 2: Yeah, well, and I think with the enums, if you are using it only in type references, it's going to strip it. The TypeScript compiler will strip any of the enum types if you don't use them as a value.

[00:17:47]
Whereas if you use them as a value, then it will. It has to force it as an object, essentially.
>> David Khourshid: Right? Yeah, I believe so. But in this case, because we're, in the actual code, output is going to output as these strings, literally. So, yeah, but, yeah, so that, that's actually a good example of, like, if we go to finite solution, over here we have this stuff.

[00:18:17]
So you could actually say flight status. This could also be an enum too, so const enum flightstatus. And then you have idle submitting error success. And so instead of, for example, checking if something is submitting, like over here, you could just use that enum. And, and so from what I've seen when working alone, enums sort of get in the way because it's a little bit more boilerplate to write.

[00:18:46]
But when you're working with a team, it's actually pretty useful to have that enum and sort of enforce that with your entire team. So I think I lean on the side of using const enums over just plain strings. But when you're starting out, just start with the string and then refactor to use that enum, or let cursor do it for you.

[00:19:08]
>> Speaker 3: How do you decide on the relations between the data types? So, for example, you put an array of destination IDs on the todos. Is the reason you did that instead of putting an array of to do IDs on destinations? Or is it pretty much the same?
>> David Khourshid: You mean when we first had the state?

[00:19:33]
>> Speaker 3: When we were building the one we were flattening it out.
>> David Khourshid: Right, so the first one before the flattening. And that's because a destination can contain many TODOS and the TODOS cannot contain many destinations. So it is a one way thing. But I mean, it does get tricky.

[00:19:48]
So like for example, if you have, if you have friends, like you want some model friends, like this friend has all these friends and this friend has all those friends. The friends can be friends of each other. You have a many-to-many relationship. It's like, okay, so how do we represent that?

[00:20:04]
With the nested data structure, it becomes a little bit tricky because it's like which one is a child of which? And then you have a lot of data duplication. Whereas if you have a flattened structure, then you could just represent the relationships like friend of or just a separate, I hate to say join table, but you know what I mean?

[00:20:22]
Like just somewhere where you're keeping track of the relationships between those different entities.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Start a 5-Day Free Trial