
Lesson Description
The "Best Practices for State Optimization" 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 covers key principles of effective state management, including using events as the source of truth, pure functions and immutability, framework-independent logic, state machines to avoid invalid states, and declarative side effects.
Transcript from the "Best Practices for State Optimization" Lesson
[00:00:00]
>> David Khourshid: The next two lessons we are going to be working on optimizing state management and really organizing it a lot better. So what I want to start off with are a few principles that I personally believe about state management that really help me shape how I think about managing complex app logic and how State is supposed to update in your application.
[00:00:27]
The first one is that events are the real source of truth. If you think about when you make a deposit or make a withdrawal from a bank, how does it keep track of how much money you have in your bank accounts? And how is it sure that it doesn't go out of sync?
[00:00:46]
It's not like it's keeping this big object, global object, where it's like, okay, this person has this much money in there. Instead it keeps what they call a ledger. But, you know, in our case, we could refer to it as just an array of events. So basically anything that happens in your application is based on events that change the state over time.
[00:01:11]
So like with the bank, you could have a long ledger where it's like you made this deposit and this withdrawal and this deposit, et cetera. And when you reduce all of those things, you come up with, like we talked about in the first exercise, that's derived value. So that's why I say events are the source of truth.
[00:01:33]
And that's why it's good to think about things in terms of events. Because since they're the source of truth, it doesn't matter what kind of sources your data are coming from or what is updating what, everything can be considered an event that changes your state. So I remember earlier someone was mentioning that.
[00:01:54]
I think it was you, that you were mentioning that you have multiple, multiple places or multiple sources of truth where your data was coming in and updating the TV app. So all of those could be considered an event where the event says something changed and the state will react to that change.
[00:02:16]
And by abstracting things in terms of events, you don't really have to think too deeply about what kind of source it is, what kind of library or API you're working with, because at the end of the day, it's just an event. So I really like events too, because events capture a lot more than state's changes.
[00:02:38]
And you're going to see this when we get to that dreaded chained useEffect example, because when we're dealing with useEffect, for instance, the way that useEffects just rerun are based on the dependency array. And in the dependency array, we're saying one of these values changed. We don't know which one, but one of them changed.
[00:03:01]
So we're missing a lot of information. We're missing the intent, what caused the change, we're missing timing details and we're missing that log of, here's all of the things that led up to this change. It's just this piece of data updated and good luck finding out where it is.
[00:03:21]
So events are the real source of truth, whether you're using them explicitly in your apps or not. You could conceptually think that anything that changes in your app is due to some events. Whether it's from the user or from some external source, doesn't matter. The second one might be pretty obvious, but the use of pure functions and immutability.
[00:03:49]
I really like using pure functions for AppLogic just because pure functions are the easiest thing to test. And it's also the simplest way of representing your applogic in terms of, when I'm in this date and this event happened, what is the next state going to be? So again, it doesn't matter whether you're working in angular react vue, etc.
[00:04:13]
You can represent your core application logic using these pure functions. And you could also represent the way that you derive state as a pure function too. So the core principle of this is really deterministic behavior. You have this pure function that runs deterministically and given the same input, it's always going to produce the same output, but more so, it's easy to test.
[00:04:41]
So you could take that pure function and write unit tests for it. And pure functions are also composable. And you can do things like memoizing the results of pure functions pretty reliably. Since everything is self-contained within that function, you know they're deterministic. So you know that if I cache this expensive value, it's always going to be consistent with what the pure function was going to produce.
[00:05:06]
So that's why I say try to keep all of your application logic centralized in pure functions. Not a single pure function, but it could be many pure functions. Another principle that I live by is the idea that you should write code as if the framework or the tool or the library or any of the underlying abstractions are just going to change under you.
[00:05:31]
Now, we've all worked in large code bases before, and legacy code bases. We know that this is almost never going to happen whether we want to or not. But the idea is that we want to avoid tightly coupling business logic to to framework specific patterns. And this goes hand-in-hand with peer functions for AppLogic.
[00:05:53]
We want to separate our logic so that it's easy to test and it's easy to adapt pretty much anywhere. Then, of course, there are state machines for modeling, which combines the idea of events being the real source of truth, first date, and also using pure functions to represent app logic.
[00:06:16]
Now again, I'm not saying use state machines everywhere, but thinking about your app in terms of these states can allow you to prevent impossible states and impossible transitions. By an impossible state, I mean, for example, let's say you have an is success and is error. You probably don't want those two things to be true at the same time, but if they're both floating around as you state, then it is very possible that that can be the case.
[00:06:45]
Another one that I see very, very commonly, and you probably have seen before too, especially with limited Internet connection, is you might click on a user or click on some search results and you will get a flash of no search results found or this user does not exist.
[00:07:02]
And then one second later they pop up magically. And so that's why, thinking of state machines really eliminates those kinds of problems where listen, all of your tests and all of your end to end tests are probably going to pass and the user might not notice. But it's those things that really help you make your app more maintainable and more predictable, not only for the developer, but for the user.
[00:07:26]
Because we don't want to see inconsistent states, we don't want to see an error that immediately disappears. Or we don't want to run into a state where we can't get out of such as an error state that requires us to restart the entire application. So using state machines for modeling, which like we looked at at the last exercise, modeling states, events and transitions are going to be really helpful for avoiding these kinds of invalid states, invalid transitions and other things that we'll run into when we don't explicitly model our app logic.
[00:08:04]
Then the last one which we're going to be talking about probably at the end of the workshop is declarative side effects. The main principle of this is we want to separate updating our state so that peer application logic from the actual execution of the side effects. A great example of this is the ELM architecture, where with ELM and you have this idea of when you're in a state and you have some sort of action or event, it's not just going to return the next state.
[00:08:38]
But it's going to return the next state and also what it calls commands or effects that are supposed to run when that state is executed. And so by having that be declarative, we could both easily test that these side effects are going to occur. And also, we separate the pure logic, so the pure function from the execution of the side effect.
[00:09:05]
So it makes it a lot easier to test. We don't actually have to test that, this side effect did something when we ran this function. So let's make sure that it did the right thing. We could just test ahead of time like this effect is going to be called.
[00:09:19]
And also, it just makes the behavior more predictable. For example, let's say that we were in a flight search page, and we start searching. We don't just want to go to a flight search results page, we want to go to a flight search results page and kick off the searching process.
[00:09:36]
And so that's where declarative side effects really come in handy.
Learn Straight from the Experts Who Shape the Modern Web
- In-depth Courses
- Industry Leading Experts
- Learning Paths
- Live Interactive Workshops