State Management at Scale in React & Next.js
Watch on YouTube (MasterDotDev)
What is this talk about?
“We could just select the hotel id, which is a string, change this to the hotel id and then we could apply our first pattern which is deriving that state.”
“The problem with selecting the entire hotel is that because it's no longer derived from that data and instead it's redundant data.”
From the opening of the talk.
Key takeaways
- The problem with selecting the entire hotel is that because it's no longer derived from that data and instead it's redundant data. 3:00
- The idea is that you want to, at a type level, ensure that this receipt exists. 8:00
- Instead of this, we could set CoffeeOrder to status success with the receipt. 9:00
Quoted from the transcript.
Full transcript
From the uploaded captions. Technical terms were normalized; some errors remain. 2,143 words.
0:00 Introduction
Hi, I'm David Khourshid, founder of Stately AI and creator of XState. I created this course because I've spent many years helping teams solve their most complicated state management problems. And I've seen the same patterns cause the same headaches over and over again. In this course, we'll cover the principles that prevent these problems. You'll learn lots of patterns, including testing data, fetching multi step forms and refactoring complex logic. We'll cover when to use states versus useReducer, how to avoid useEffect pitfalls, and how to choose between local state, URL, state and global stores, add features, confidently prevent bugs before they happen, and create state management that scales gracefully with your team and app. Working with redundant states so just like with derived states, we want a single source of truth for all of our data. And so with derived state, we saw that in like, okay, a total can be computed from a list of items. But like I said, sometimes it's not so clear. So an example with redundant state that I see a lot, let's say that we have a hotel selection. So we have a bunch of hotels that we're looking at and we want the user to be able to select.
What we would typically do is say, okay, they want to select a hotel. It makes sense in our minds. We're going to let them choose a hotel and then set the selected hotel directly. The problem with this is that we are creating redundant data. So we have a selected hotel, which is an object that's exactly the same shape inside the hotels over here. And instead we could actually derive that data, so I'll just show you how this works. Copy this over. All right, so instead of having the selected hotel, what I want you to do instead is have the selected hotel id, because we could derive the selected hotel from that. So if we change this to set selectedhotelid, now this becomes a string instead. So we're no longer doing that. And so now we could just select the hotel id, which is a string, change this to the hotel id and then we could apply our first pattern which is deriving that state.
So const state selected hotel and then we could just find the hotel based on its ID. Now this is actually really useful because let's say that you're selecting a hotel and you also want these hotel items to dynamically change. They might change for some reason. The price might shoot up or a date might become unavailable. You might be fetching this data from an external source. So the problem with selecting the entire hotel is that because it's no longer derived from that data and instead it's redundant data. You might be showing stale data. So that's one of the biggest dangers of selecting the entire object. And this just makes the logic both reading and updating a lot simpler because now all you have to worry about is an id. Another thing that I really want to demonstrate here, and what we're going to 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 coffee order.
So let's say that we have const coffeeorder set coffeeorder. And this is just. Yeah, oops. Some use data over here, we're ordering a cappuccino and we also want to model actually making this order.
4:00 Instead of this, we could set CoffeeOrder to status success with…
So this is probably muscle memory to a lot of React developers. But you're going to start with your const is loading, set is loading and then maybe const issuccess. So you have that boolean and then const error, set error. And then you're going to actually kick off the ordering process probably in a useEffect. 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 do something, it's going to make a fetch request. So let's actually do a demo fetch request over here. So mock fetch call, so we might do something where we're fetching that order and then we have all of 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. 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 cancelled, 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. 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 can eliminate both of these and just set status to success because those are mutually exclusive. 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 going to set the status to error. And so that's one way that we could really simplify, just simplify handling multiple boolean states that are really meant to represent one kind of state, now if you want to go above and beyond, there's something called, or there's a pattern called type states.
With type states, here's the 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. 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 like 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, or even if you have issuccess, you know 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 exists. So in TypeScript you might add an exclamation mark. It's actually totally fine if you add multiple exclamation marks in some cases. 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, type coffee order, and we're going to be using in TypeScript what's called a discriminated union. So instead of just a plain status string, we're going to put it inside of this object. So we're going to 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.
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 receipt is null. But when we have successful 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 coffee order, 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. 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 receipt, 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. 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 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. And so that's what type states are very, very useful for. I want to briefly talk about fetching data because when you look at your applications, or at least many applications in React that I've seen some of the most, I don't want to call it egregious, but some of the most common uses of useState and useEffect are for fetching data and React. It gives you actually a couple patterns or, sorry, React naturally gives you a pattern right now to prevent that.
11:00 We can actually use a library like Tanstack query
And that's using suspense and using server components. Now if you don't have the ability to do that, like you're not on the latest version of React and that's not possible. One of the more common approaches that you could do is using a third party, third party data fetching library. So I'm going to go to fetch readme. And so commonly we see like this useEffect and useState dance when we're searching for results, you've probably seen this code all over the place. This is just a very common example of how you have accidental complexity. This has almost nothing to do with the problem domain that we're working with. In fact, the only line that matters is this one right here. Well, this one and the actual flight search. Everything else is just noise. And so this is an example of how it just makes things really confusing to work with even though like it's an extremely common pattern.
So what can we do instead? We can actually use a library like Tanstack query. With Tanstack query, it has so many features that if you were to build your own data fetching abstraction, some of these features are things that you might run into in the future and realize that, all right, Tanstack query already has this built in, such as being able to define how long to keep things in the cache or automatic retries and things like that. So how many of you have actually heard of or are using Tansa query in your applications? All right, 50%, that's, but yeah, and I'm sure that many of you have honestly just done this useEffect pattern and even this isn't perfect. Like we need to set let is cancelled to like false and then we need to do some cleanup. So return iscanceltrue, this used to be like the way that you would fetch data and useEffects the proper way, but now instead of that, the more modern way is like const aborts controller.
You set up an abort controller and then you have abortcontroller aborts and then you could either pass it in natively if it supports it, or you could do something where you if like if the signal is aborted then return. It gets really confusing. And these are things that you're typing over and over and over again. And that's why it's tempting to make your own abstraction or to just copy paste this entire thing. And even when you ask AI coding assistants, they're probably going to create a useEffect use date mess just like this for fetching data, but instead of that, we could just useQuery. So with Tanstack query, it does require a query client and I'll show you where that's set up, but you give it a query key. And so this is going to be an array that uniquely identifies the data that you're loading. So we scope this by like we're seeing that this is flights and we pass in the flightsearch data so that if the flightsearch data changes and we refetch again, we know that we're not going to reach into the cache because there is no cache item for this, we're going to have to make a new search, which happens over here.
And honestly these are pretty optional. There's also mutations if you want to instead of fetching within a component, you want to just fetch directly when some side effect is supposed to happen.
Related talks
-
useWat
React Rally 2023. 32:41, 2.7K views -
useWat² (with Jenny Truong)
React Miami 2024. 27:55 -
useWat
React Alicante 2024. 33:06 -
useState Is Over
PodRocket (LogRocket podcast), 2025. 42:01, 1.2K views