David Khourshid

Goodbye, useEffect

Conference talk at Reactathon 2022. 26:00 long, 240.2K views on YouTube. Topics: react hooks, side effects, state management, react, event-driven architecture.

Watch on YouTube (Real World React)

What is this talk about?

“Where we could say, like, if these things change, then I want this useEffect to run, and we also have component will amounts.”

“One of the reasons why we tend to use useEffect is because the imperative approach to doing effects is when something happens, execute this effect.”

From the opening of the talk.

Key takeaways

  1. One of the reasons why we tend to use useEffect is because the imperative approach to doing effects is when something happens, execute this effect. 6:00
  2. The problem is when we put effects inside of our components, like rendering life cycle, then we run into the problem. 12:00
  3. Hey, this effect is supposed to be executed when we transition from this state to the other state, and these effects aren't executed by the way. 15:00
  4. Inside of useEffect and other state management and React libraries work actually the same way, where basically you're delegating control of the effects over to those libraries instead of you having to do it yourself. 19:00

Quoted from the transcript.

Full transcript

From YouTube auto-captions. Punctuation and capitals were restored automatically. Technical terms were normalized; some errors remain. 4,304 words.

0:00 Introduction

Foreign [Music] Hello everyone. How's everyone doing good? Yeah, how many of you feel like this right now? Happy, right? Okay, awesome? How many of you? That's the wrong one, all right? How many of you feel sad? Okay? How many of you feel, you know? Okay, like, a little bit curious about what I'm going to talk about, all right, yeah, you're like, why, you know? Why are we talking about useEffect, and why should we get rid of it? So we are going to be talking about everyone's favorite hook today, which of course, is useEffect, and so I know it might not be everyone's favorite hook. But we're going to talk about it because it, personally for me, has caused a lot of problems. And especially like, even from when I was starting with React and React hooks and even till now, like, there's just a bunch of things that can go wrong.

So, first of all, if you're struggling with useEffects, don't worry, because even senior reactives, you know, struggle with it. People who have been doing reacts from the very beginning, you know, and even those people we all have the same experience like with React hooks. Because, if, honestly, if you've done class components, it's like all that knowledge sort of goes away once you. Once you've started using hooks. But that large dependency array, you know, the ones I'm talking about, the complicated if statements, I mean, those are typical, you know, it's just a really awkward hook.

Play from 0:00

2:00 You know, if we pass an empty dependency array, you know…

So that's what we're going to talk about today. And if I wanted to summarize my slides, like, if you have somewhere to go and you want me to just summarize it real quick for you? I'll just say. useEffect is not for effects now, so this might actually leave you a little bit confused. So, where, like, how did we get here? So when we were doing effects and, you know, class components, how many of you remember that? Where are my elderly? React devs? Okay, so we had three places to put them, we had componented mounts, right, we had componented update, which is, whenever we had state come in. And or some props change or something. We're like, Okay, that's a really good opportunity for me to fire an effect, or component will. Unmount. And so this is a good place to do on subscriptions or to do some sort of cleanup. But these were the three main places that we had in class components to do effects, so now what do we have?

Do we have those same three places? Well, with the useEffect hook? You know, if we pass an empty dependency array, you know, we have componented mounts, sort of we're going to talk about some foot guns there in a bit, we have componented update. Where we could say, like, if these things change, then I want this useEffect to run, and we also have component will amounts. Sort of like you have this empty dependency array, it's really awkward. You have a function inside of a function, you know, we're not Haskell, what are we doing here? So I just want to say that useEffect is not a life cycle hook. And so a lot of us think about, like, okay, effects we're used to, you know, just putting them in life cycle hooks. But useEffect is not really for that purpose. And React 18 actually makes that really, really clear. Also, the default behavior of useEffect is an infinite loop.

And so you might be thinking like, Okay, I've been using effect enough to know that I need a dependency array, right? And so everyone should know that. So why does everyone run into this like infinite loop scenario? So, here's an example. Like, I just have an example where we set count, count plus one. You might think, okay, this is a side effect, this is only going to run once, but in reality we have this happen. Okay, so, useEffect is also not a state setter. You could set state in it, but if you need to set state, you could do that in other places too. Okay, this has been like one of the biggest problems, especially in the very beginning, when I was making API calls. And when you put fetch requests in there and they start looping infinitely, things go really, really wrong. So, this is another example. And this is actually straight from the React docs as it exists today, not the beta docs, but the actual React docs.

Where do you see something wrong? Here? There's no dependency array. So this, this seems like, I flew here all the way from Orlando. It really feels like, when I went on the plane and I saw no one with masks, you know, it was a little bit dangerous, a little bit uncomfortable, and, sometimes ugly, so it just feels weird. But thankfully, the React documentation does introduce this idea of a dependency ray. But it's at the very bottom, so you know, you have to scroll all the way down and it's a tip. It's like, Hey, here's a cute little tip. You could, you know, use a dependency array, but now all of us know like, we need that. So, here's what we really expect when we useEffects. Now that we're used to all these foot guns, we think we're gonna do this effect. Whenever something here changes in the dependency array and we're like, Okay, that's pretty simple. I, I could do that, I could, you know, manage my effects that way.

But the reality is a little bit different and my slides aren't exactly working, but that's okay. So the reality is more that we, you know, we have a big if statement in there and we have a big dependency array, and it's just a little bit weird.

Play from 2:00

6:00 We have action effects, or what I call action effects

And so. One of the reasons for this. And one of the reasons why we tend to use useEffect is because the imperative approach to doing effects is when something happens, execute this effect. It's like a do this, like that's the definition of imperative, so what's the declarative approach of useEffect? Well, when something happens, it will cause a state to change, or props to change, or something in the component to change. And depending on which parts of that state changed. Which is a dependency array, this effect should be executed, and that should only happen if some condition is true. You see how confusing that is. Oh, also, React may execute it again because of concurrent rendering. Does, so, like, Okay, we? We have imperative, we have declarative, but really, it's more like this is implicit, or this is explicit, and this is implicit, and I prefer explicit over implicit. So, yeah, you know, when we useEffect, we have this like, just massive, you know, crazy, boolean, jumble and you know.

How much I love my boolean variables, like I see them all the time in useEffect. And here's a few examples, by the way. I'm not going to name any of the companies. But I found this just an open source repos. And really, it's not their faults because used effects, just an awkward hook. You know, we have these conditionals, really long dependency arrays. Here's another example. Here's one of my favorite examples, which isn't actually there. I if you want, you know, come find me, I will show it to you later. It's by again, one of the companies I will not mention, but it's a really fun one. So where do effects go? You know, I, I'm talking a lot of, you know, crap about useEffect. So where should these effects go? You know, when we're looking at the components, we see there's a few possible places, right? We know that we can't do any side effects in render.

We already know that, you know, we could use useEffect. And we're going to talk about when you could use it. But it is a little bit awkward. So where's left, maybe outside the component? What does this look like? This is weird, like, you know, we're supposed to have everything inside our components, right? So, yeah, if you decide, you know what. This actually is a good use case for useEffects. Like, I'm listening to what David says. But I'm going to put it in there anyway, because that's just what I'm used to doing. Here's the problem. React 18 will execute your effects twice on Mount in Strix mode. How many of you have used React 18 have been bitten by this already? So a few of you, the others you know you, you will. You'll experience it, it'll be bad, and so what happens? What React 18 is doing is we're mounting the component which will cause the effects, then we're unmounting it, which, it is simulated in strict mode and so that will be a cleanup of the effect and then we remount it.

And so it's going to be executed again. And so the reason it does this is because, it's trying to tell you that you're not using useEffect for its intended purpose. Which is what I'm going? To talk about. So it's really sussing out all of those effects that really shouldn't be there, so useEffect really feels like use defect at this point. And also like a foot gun. So you're like, why am I using this hook? You know, it's a. And what is useEffect really used for, anyway? So the answer is useEffect is supposed to be used for synchronization, and I suggested that useEffects should really be called use synchronize because you're supposed to use useEffect. That is really confusing to say, by the way, you're supposed to useEffect when you have something that you need to synchronize with. So, examples would be a subscription like this. And also Dan says this as well, I feel like my sites have no credibility unless I have a Dan tweet on there.

So he says this as well. That if you don't use useEffect, you know, or if you're trying to manage your data with useEffect, you're gonna have a lot of problems. It's really used to synchronize it with some external system. But another classic example is, just tracking things in the DOM. So adding event listeners like a pointer move. Doing your cleanup and trying to keep this dependency array as small as possible. So we have two types of actions or effects, really. We have the synchronized effects which we just talked about, those are the subscriptions, the event handlers, all of that. And we have action effects, or what I call action effects. These are your fire and forget effects. And again, with React 18, you are going to have that unmounted remounts happening through the strict mode, and if you turn off strict mode, React 18 is going to do it. Anyway. Because of some concurrent rendering, so this is just something you have to be careful of.

But typically, effects that are based on synchronization are safe to stop and remounts just because they're not those one-off action effects. So then where do action effects go? And the answer? The official answer is actually event handlers. So whenever possible, you want these effects to go inside of things like on submit, on click, things like that. So I both like and don't like this answer, even though it's the official answer, because we're doing. Imperative logic. It feels like it's inside of render, but it's not, it's actually happening outside of render. And also you have implicit code in your code base. So, when we look at the beta documentation, which hopefully you've all read, they're excellent. One of the things it says is that event handlers are the best place for side effects, and so further down the documentation. When answering the questions where you can cause side effects, it says that these side effects usually belong inside the vent handlers.

And so when, you know, when concerning useEffects, like when do we actually use useEffect? We say that. Or the documentation says that this should actually really be your last resort. So you heard it from the documentation, maybe you heard it from me first, maybe you should read the documentation before coming to my talk. And I wouldn't have to talk about this, but I actually really enjoy talking about this. So, yeah. The problem is when we put effects inside of our components, like rendering life cycle, then we run into the problem. Like we talked about in React 18, where these effects can execute multiple times because they're at the mercy of the component rendering. But if you put them in the event handler now, you don't really have that problem, like it doesn't matter how many times that component is going to render. These effects are actually going to only happen once because it's guaranteed. And reacts that when you go from state to state, talking about this like a state machine, then these effects.

This transition will only happen once, but rendering might happen multiple times, so effects can, or effects happen outside of rendering. But obviously I have a little bit of a problem with this. If you start putting your effects inside of event handlers, then you get into some messy, situations where it might look like this. Again, my slides aren't showing all the slides, but, this eventually expands. Where you have is loading, you're doing set, loading, set error. You know, you might do a fetch inside of there and you might do a lot of state management. And all of a sudden, these event handlers become multiple lines long, where you really want to keep it just to one line, so. In React, a popular mantra is that UI is a function of states, and so that whatever state you have, it's going to render a UI. And so we could actually derive, this whole idea, which Redux popularized, where given the states, in an event that happens now, you have a next state.

So the problem with this, though, is that you really have no place for effects, it doesn't really fit into this function, so where do effects happen? Well, you know, we have many different answers from many different state management libraries like middleware, callbacks, Monads. I think, but the real answer is straight state transitions, and it always happens in state transitions. And so the reason for this is that the only way that you could trigger an effect is to do something, so something needs to actually cause that effect to happen. Effects can't just happen out of the blue. And so typically, if you catch it within the event handler, then you know, that's that's the a great place for an effect. So instead of just this whole idea of a state, and when you get an event, you get a next state, we have states, plus events equals next state. And effects, so this is called managed or declarative effects. Where? By the way, if you have your bingo cards out, I am talking about state machines, so you could mark off that box.

But state machines like this, this model over here of, you know, state and events equals next and effects. This is all you need to understand when, talking about a state machine like these, this is how state machines work. So in a state machine, you have an initial state, and you might have a finite number of states. And in the transition, that's where you define the effects, so you could say when this event happens, like load, then this fetch data effect also happens. And so, you know, that equation just becomes a really convenient way to define. Hey, this effect is supposed to be executed when we transition from this state to the other state, and these effects aren't executed by the way. They're basically handed off to something that's going to interpret that and say, okay, I'm going to save the state and I'm going to take. These effects and I'm going to execute them. So your state machine, these transition functions still remain pure, and so there's, some pretty cool features with state machines as well.

You can see that the fetch data is happening in both the load and the reload state. And so, state machines have this idea of entry, exit, and transition actions as well, so it's pretty convenient. So where? How can we do this? Like, how can we eliminate useEffect and actually execute effects while we're, you know, dispatch, or like, just handling events? So I have this, hook here called spicy dispatch, and no, I'm not going to make it in npm library or something like that, I'm just putting it on the screen to be spicy. But our dispatch here is a little bit fancy because inside the dispatch itself. We're getting the next states and we're determining which effects should be run, given the state, the event and the next state. So basically, that's the transition, because those are the three parts of it, and we're executing those effects now. This is, you know, this is left as an exercise to the reader. But you could just, you know, say, okay.

When the state is loading and I just came from the idle states, then I'm going to execute this effect. But the main point here is that all this is happening within the dispatch itself. And so, when you think about when you dispatch stuff, that's happening. Right? In the event handler, all right, so where do action effects go? event handlers? Right? Technically, sort of not really. So they really happen in these state transitions, but these state transitions, especially in React, just so, happen to be executed at the same time as these event handlers. So, as a step one, I would say, try to move all of your effects away from useEffect and put them in those event handlers. Unless, of course, their, you know, their, synchronizing effects or activity effects. So, a better way to manage this, at least in my opinion, is to use external stores. And so I don't really recommend. I love this hook, by the way, using external store, but I don't recommend you use it directly, so, using external.

Store is really cool because you have a component and this component can communicate with a store in which it could sync with it and it could also interact with it. Because the store might have methods such as dispatch or send, or whatever. And this door can be the thing that both holds your state and executes all of your effects, so it's pretty nice. Here's an example of its usage. The API is actually pretty straightforward you grab some store from somewhere, I know that React Redux uses a using external store, I believe. And also X8 as well, and you provide a way to subscribe and to get a snapshot of that, of that state. And then you could, of course, interact with it in any which way you want. So, X8, of course, is my library, it's a library for state machines and statecharts. And it also lets you declaratively manage your effects, too, and so X8 is using sync external store under the hood as well.

This allows XDA to be in total control of executing these effects, so these effects will execute without you having to do anything. Inside of useEffect and other state management and React libraries work actually the same way, where basically you're delegating control of the effects over to those libraries instead of you having to do it yourself. So in XState, just like we talked about with entry, exit, and transition actions, you could find those right in your. I'm just going to call it a fancy reducer. It's a machine definition. You could define your entry and exit actions, your transition actions, and even invocations, which are sort of like those synchronizing activity effects that we talked about. These are the ones where if you weren't using a state management library, you would typically put in useEffect. So, when it comes to state management, you know, we think about state stores. We think about state updates and subscribing to data and getting new data as it comes in, and events dispatching.

But in my opinion, this is only part of the picture, you know? We also want the ability to communicate with multiple stores, communicate with those stores like send events to it, receive events from it. Effect management, which is, you know, the most important part of this talk. And also, you know, maybe even handle finite states and transitions, and this, to me, seems like state management, plus, plus now we're talking about state orchestration.

Play from 6:00

21:00 React, query, SWR, because you're supposed to…

So, I'm going to go over a couple more patterns here. Sometimes it's actually really hard, if not impossible, to find that event handler and to execute an effect only once in there. So what you have to do is you have to do this weird. What's called a ref flag? And so you would say, is this executed? Yet you would have a ref in there said it's a false first. And then you would start, your useEffect, and if it's already executed, you're not going to, you know, execute it again. So you do your thing and then you set executed. Ref.current is true and so, that way when your effect reruns because of React 18 doing its thing or some other reason. It's going to check that flag, which is going to be stable, so that flag will stay true. Even when your component virtually unmounts and remounts. And so this will allow you to only run your effects truly once. So, you know, just think about this as like an escape, an escape hatch for if you really want to useEffect.

And so you might be looking at this flag and thinking, Okay, this seems like a really big red flag, like, why am I using a wrath in my effects? But honestly, this is the least awkward way to do it. So, yeah, and also, what about fetching data? You know, we have fetch on render. This is one of the most popular, you know, use cases of useEffect. We have used query, use SWR, but I'm here to say that fetch on render is actually a lie, you know? Sorry to those, libraries. React, query, SWR, because you're supposed to, or at least the React team wants you to really move towards a pattern of render as you fetch. And so suspense is really, really great for this. And so this means that you're going to find that event handler that we talked about. You're going to start fetching in there. And all you have to do is read that in your components and make use of suspense, or you could ditch the UseFX altogether.

And use frameworks such as Remix or next, which make it really easy to just get rid of these. They, they didn't pay me, but they will. Hopefully I'll talk to them, all right? So, just to give you an example of, like, the code that you save with fetching data. This is a typical thing that we do. And honestly, this is what I taught at React training workshops. Where it's like, okay, you have to set this awkward cancelled flag and then you fetch some resource and then you. If that. If it was canceled due to the cleanup, then you have to make sure not to run the effect again. But look at suspense, this is all you need, and so, you know, I know you love your useQuery and use SWR, but really moving towards suspense just makes things a lot, lot easier. All right, so speaking of code save, this is our useEffect example. This is when we move it into an event handler.

Play from 21:00

24:00 State transitions are what trigger effects and effects go in event

And so, when we take this and we say, you know what? I'm going to let these effects run in some store that both manages my state and manages my effects. Then this shrinks down to this. And all you're doing in that event handler is sending an event, it's all you're doing. And so it doesn't matter which library you use to do this. Obviously. X8 handles effects. React has a new action listener middleware that does it. And I'm sure other state management libraries do it. It's just like, actually, my. My call to action for these other seat management libraries is to realize that effects are state management. They shouldn't be treated as this other thing. So don't put side effects aside and really, really manage your effects with your state management. You could do it in a declarative way, you could do it in a pure way. But just that notion of saying, here's the effects that I want to run due to this date transition that's really powerful and that's actually going to help you with testing as well.

So, in summary, sorry, useEffects is for synchronization, not for effects. Even though synchronization can be considered an effect, that's not the main purpose of useEffect. State transitions are what trigger effects and effects go in event. Handlers so find the event handler that triggers your effect and put the effect in there. Also render as you fetch with suspense and model effects with state machines. So with that, thank you, React-a-thon [Applause].

Play from 24:00

Related talks