Lesson Description

The "Finite States" 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 demonstrates how grouping related state into a single object and updating it efficiently can reduce complexity in React. He also introduces finite and type states as alternatives to managing multiple Booleans, helping enforce valid and consistent app states.

Preview
Close

Transcript from the "Finite States" Lesson

[00:00:00]
>> David Khourshid: So in this exercise we're going to be keeping those principles in mind through pretty much the entire workshop and of course also this exercise, but also we're going to be doing two things. We're going to be combining states, so really reducing the number of use states that we have.

[00:00:20]
And we're also going to be learning about type states and finite states. So, let's talk about the first concept. Let's go to a scratch pad, so finite scratch. Okay, let's say that we have an application for, I don't know, let's say that we're making coffee. So we have const coffeeType, setCoffeeType.

[00:00:50]
We have different types and it's already like just giving me a whole bunch of different options. We also have size, and we also might have sugar and milk. The thing is that all of these are actually related. So we're ordering a coffee. We do have multiple use dates here.

[00:01:10]
One of the things that I want you to get used to, you don't necessarily need to do this, but honestly, when you have a lot of use dates, it can become a problem, but group related data together. So, instead of all of this, we could just have, let's call this coffee, say coffee.

[00:01:30]
And instead we could combine all of this into a single object. So this is going to be the type of coffee, the size, the sugar and milk. And we can all have this in one single object. Now the pattern for updating the state is actually going to be a little bit different.

[00:01:49]
So over here, when we're handling the coffee type change, we're no longer just setting the single state variable, instead we're updating the entire object. So that can look like one of two things. It can either look like something where you take that existing object like coffee, and then you pass in the type over here.

[00:02:15]
So you're basically taking that entire object and just saying, I want this object with this type updated. Now there actually might be a small problem with this. This coffee is a closed in variable, so we have a closure issue, which sometimes it's not a problem, but other times we might see some unwanted side effects.

[00:02:38]
Which is why I actually recommend using the pattern of grabbing the previous value and instead using that. So that's going to use the existing coffee over here and say I want to update the type based on the previous value of it. So that is one way that we could basically combine our entire state.

[00:03:03]
So, let's put some default values here, and that's exactly what it looks like. Now, another thing that I really want to demonstrate here and what we're gonna be using in this exercise, is the idea of finite states. So let's say that we have another two and then we are making our coffeeOrder.

[00:03:29]
So let's say that we have const coffeeOrder set coffeeOrder. And this is just, yeah, oops, some use state over here we're ordering a cappuccino, and we also want to model actually making this order. So this is probably muscle memory to a lot of React developers. But you're going to start with your constIsLoading, setIsLoading, and then maybe const isSuccess.

[00:04:01]
So you have that boolean and then const, oops, const error, setError. And then you're going to actually kick off the, the ordering process, probably in a use effect. So you're gonna have a useEffect over here, and then again this is all muscle memory. You want your coffeeOrder over here, and then it's going to, it's going to do something, it's going to make a fetch request.

[00:04:29]
So, let's actually do a demo fetch request over here. So, mock fetch call. So we might do something where like we're fetching that order and then we have all this boilerplate that we have to add. We set success to true, loading to false. And then if we have an error, then we also set loading to false.

[00:04:51]
And this just gets problematic because it might seem simple over here, but we're really managing two seemingly unrelated pieces of state that are actually supposed to be related. And we also need to handle canceling. So if not cancelled, then we go ahead with this. So instead of that, I want you to, just based on our previous exercise, think about the different states that we could be in.

[00:05:23]
And so this actually makes a pretty simple refactor. Instead of is loading is success and error, we could do const status, and we could have a string enum representing the status. Now we could eliminate basically both of these, and instead we could set status loading. We could eliminate both of these and just set status to success because those are mutually exclusive.

[00:05:55]
We never want it to be loading in success at the same time, in reality we're just changing one thing. And then we have an error over here, so we're gonna set the status to error. And so, that's one way that we could really simplify handling multiple boolean states that are really meant to represent one kind of state.

[00:06:21]
Now if you want to go above and beyond, there's something called, or there's a pattern called type states. And so with type states, here's a problem that it solves first of all. Let's say that we're trying to render something and we have if status is success. So, let me add another variable here we have const receipt, set receipt.

[00:06:50]
And this could either be a string or null. So, I want to show the receipt, but again the receipts. Actually let's do this. Let's make the receipt a total. So, a problem with this is that, and it doesn't matter if you're using finite states or if you're just doing something in your application where you are 100,000% sure that this receipt, when the status is success.

[00:07:16]
Or even if you have issuccess, you know that that receipt is going to exist because, let's say that we set the receipt over here. So we're setting the status and we're setting the receipt. You know that that exists. So in Typescript you might add an exclamation mark. It's actually totally fine if you add multiple exclamation marks in some cases.

[00:07:40]
I discovered that randomly. But the idea is that you want to, at a type level, ensure that this receipt exists. So, how exactly can we do that? Well, we can use type states. So type, let's see, CoffeeOrder. And we're going to be using in Typescript what's called a discriminated union.

[00:08:05]
So, instead of just a plain status string, we're gonna put it inside of this object. So we're gonna say status is idle or loading or success or error. And this is actually a good way to start out. You have the error, and then you have the receipts inside the same object.

[00:08:23]
So from there you could actually transform this to a discriminated union by adding this pipe and then saying status success. So now we could actually get rid of this, and we could say that the receipt is definitely defined over here. So we could say that in this case the the receipt is null.

[00:08:48]
But when we have success, we definitely have the receipt. And so here's how this looks. Let's actually just put this somewhere else. So let's const CoffeeOrder. And then we have idle. We have no error and no receipts. And I'm actually going to delete this over here. And so instead of this, we could set coffeeOrder to status success with the receipt.

[00:09:20]
And here is the really, really cool part about this. This enforces the data in two places, both when you're setting it. So if I get rid of receipts, it's actually going to yell at me and say, you're missing the receipt here, you're saying it's success, but the receipt doesn't exist.

[00:09:40]
So it's really forcing us to keep that state consistent. And then over here, I no longer need this or I have to do coffeeOrder.receipt.total. So if I say coffeeOrder.status is success, then I know for a fact that that exists. So if I change this to error, I know that we don't have a receipt here, or at least we can't read the total because it might possibly be null.

[00:10:09]
And so that's what type states are very, very useful for.
>> Speaker 2: In the first example, when setting the coffee state, you mentioned using previous because splatting coffee had some pitfalls, which by the way, I think this is the answer to the previous chat's question about setting a profile.

[00:10:28]
Send the name and profile, this is what they were wondering how to do this. But what are the pitfalls of just spreading coffee versus using the previous statement?
>> David Khourshid: You might run into closure problems with that because you might have a stale version of coffee. No one likes stale coffee, especially if you microwave, it tastes terrible.

[00:10:49]
But if you have a stale version of a value, then when you set coffee, you might accidentally revert to that previous version or at least different properties of that previous version. And this can show up in many cases, if you put this in a used memo and decide to memoize it excessively.

[00:11:07]
Or maybe it's in an event handler that does not change for some reason. Or even in a useEffect, there's just many places where that might come up. And so that's why, honestly, as a best practice in a habit, just try to go based on the previous value inside of that function, because over here you're guaranteed that that value is going to be the latest value value of that state.

[00:11:31]
>> Speaker 3: I'm thinking of a scenario where some related states will be updated in one context provider, but a smaller consumer component may only use one part of the data. So will that lead to unnecessary read renders?
>> David Khourshid: That's a very good question. So, it definitely depends. And this would be a good use case for useMemo.

[00:11:52]
So you could just see if all of those values are the same. But honestly, it's not a big problem in practice. I wouldn't recommend you to just put every single use date inside your component inside of a single use date. Instead, I want you to separate it by concern.

[00:12:11]
So for example, this coffee object, this is an object that we could pass around. And so chances are that if we change the size or any part about that coffee, the entire object fundamentally changes and so we might want to let components downstream know about that change. But in practice I haven't seen this to be a problem.

[00:12:37]
So, always just double check before you decide to put any optimizations in place. But just know that there do exist some optimizations in react like useMemo, etc., for that. And then Masij asks in CoffeeOrder, did we use generic type as the first variance because the union was not exhaustive?

[00:13:03]
So in reality, yeah, you might wanna flesh this out a little bit more. So for example, if we're loading, then we know that we don't have a receipt, we don't have an error, but if there is an error, so we definitely have an error and there's no receipt.

[00:13:23]
So then we could just get rid of these and we have our complete type state over here. So this is more around the lines of what it should look like.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now