Can useEffect affect your codebase negatively? From fetching data to fighting with imperative APIs, side effects are one of the biggest sources of frustration in web app development. And let’s be honest, putting everything in useEffect hooks doesn’t help much. In this talk, we'll demystify the useEffect hook and get a better understanding of when (and when not) to use it, as well as discover how declarative effects can make effect management more maintainable in even the most complex React apps.
All right. Hello everyone. My name is David Khourshid, and my slides will be showing up shortly, I'm sure. Oh yeah, there it is, okay. David Khourshid, I'm at davidkpiano pretty much everywhere. I'm very excited to be in London again. I work at Stately.ai, where we think a lot about state, logic and effects. But enough about me if I keep going, then this talk is going to last longer than your next Prime Minister, so let's get on with it.
[00:41] Okay, so of course I work at Stately and one of our engineers, Matisse, you might have seen him, he's in your NPM, your node modules, I guarantee it. He's one of the most talented developers I know. Yesterday he created a PR where he asked, "Am I 100% confident in this fix?" Hell no, it's based on useEffect. So we're going to be talking about that today, our favorite hook, useEffect.
But let's start from the beginning. Who remembers class components? So yeah, some people really want them back, so do I. All right, so in this example, I remember that when React came out, I was very excited for it. I'm like, "Wow, this is going to change everything." And React did change everything on every single render. So over here we're doing what most components do or what most reactive developers do, which is fetch data and show it on the screen. I admit that's 90% of your job, but we had a nice convenient little lifecycle hook componentDidMount to do that in. But now things are different, we have useEffect. So I remember I learned about useEffect in 2018 and I was very excited to use it, because I'm like, "Wow, this makes things a lot simpler." So instead of making these big bulky classes, we have these hooks that we could put things in.
[01:58] I wanted to fetch data. It was 2018, so I wanted to use new JS syntax and it was an effect, so I put it in UseEffect, tried this, React yelled at me. It said, "Listen, this async/await stuff, it's fancy, but you cannot use it like that. You have to..." It does infer that, "Hey, I don't know what to do with this promise that you returned from useEffect." So we had to do things like it was 2015 and use .then. So I fetched some data, then I set the data, I'm like, "This is simple. I just put it in the useEffect. I get my data, I'm good to go, right?" Wrong. Guess what happened to my AWS field the next day? Yeah. So this by the way, it will keep fetching and fetching and fetching and React won't tell you that, "Hey, this is going to occur as an infinite loop."
All right. So at this point I'm like, "Okay, maybe I should read the documentation a little bit." So I scrolled all the way down. And so there was a tip, it's just a tip. This very crucial bit of information is a tip in the current React docs, optimizing performance by skipping effects. And so I learned that you're supposed to use this little dependency array, and since this didn't really have any dependencies, you just put it empty like that. If you look at it sideways, it looks like an agonized React developer screaming into the void, yeah. All right, so I learned my lesson, I learned what the dependency array is for, and so everything was good, right? Well, now I discovered that we have something called a race condition, because when I fetch something with an ID and then I decide, you know what? I want to fetch something with a different ID. If that ID is cast, then that promise is going to be resolved first, while the first one is still going, because we didn't cancel it.
[03:58] So now we have to do the dance. This is a dance that so many of you have done before and that's having this is canceled false flag or React calls it, ignored in the official documentation. And then you have to make sure that we haven't canceled this promise. You have to have a cleanup in there and you have to do all this. So some of you just copy and paste this. Some of you put this in a usePromise or a useAsync custom hook, which is totally fine. And this worked well for a while until React 18, where effects execute twice on mounts. So when I first discovered this, I didn't know it was a React 18 thing. I'm like, "Am I doing something wrong in my code? What did I do? Did someone change something? Was it the Tories? I have no idea."
useEffect & React 18
[04:48] So what's actually happening over here? So I looked it up and saw, okay, we mount an effect and then React, it's actually simulating an unmount and then React is remounting it again. But I was wondering why the heck was it doing that? And so I did a lot of digging around. I was looking at the second official React docs, which is random Twitter threads by the core maintainers, and I realized that useEffect, it felt like a defective thing. I'm like, "We really, really shouldn't be using it. And the more we use it, the more we feel like this, right?" But of course I calmed down a bit, researched a bit more, learned about how useEffect works. And so the answer might surprise you. It doesn't…for many effects, it actually does work. But there's many telltale signs where you could see, "Okay, maybe I shouldn't be using useEffect."
First of all, React 18’s double exec, that thing that it does only in strict mode and only in development environments, this is an eviction notice, basically. When this happens and part of your app breaks, it's basically React telling you, you shouldn't be using this effect inside of useEffect. Also, there's other symptoms, like long dependency arrays. When you have complex conditionals inside of your effects, if you're missing cleanup functions or if those cleanup functions themselves are conditional or you might have adhocs that state calls just littered throughout the place, these are all pretty telltale signs or side effects that might indicate you're probably using useEffect wrong.
What are Effects and how are they different from events?
[06:33] Okay, which effects then should we be using in useEffects in the new Reacts beta documentation? Which I'll give a link to later. They say that there's really two types of effects. There's the Effects with a capital E, those Effects let you specify side effects that are caused by rendering rather than by a particular event. So we're going to get into the difference, well right now.
So React refers to two types of effects, Effects with the capital E and events. So effects that are supposed to happen in eventHandlers, but I think this is a little confusing. So for the duration of this talk, I'm going to be calling them activity effects and action effects.
[07:19] And so activity effects are something that is an ongoing process. It's something you don't forget about while you're in the life cycle of your component, you're intently watching it. So this might be a subscription or watching a live stream of a head of lettuce or something like that. An action effect is something where you just explicitly execute it, it's fire and forget. You don't care what the end result of that is, like Brexit.
So what is useEffect for? useEffect, the hook specifically is for synchronization with activity effects, not with action effects, but with activity effects. So one example of this is for example, a listener, a resize, a mouse move, something like that, where you create a handler, you add that handler and you make sure to use that cleanup function to say, "I don't want to listen for this anymore." And because activities are ongoing things, it's like watching television, where it doesn't matter how often you turn on and off the TV, the channel is still going to keep going. You're not imposing any side effects by turning it on and off. It's not like you turn off the TV and the BBC is like, "All right, let's cut it, this person has stopped watching."It doesn't work that way.
Where do action effects go?
[08:39] So where do action effects go then? We know that we can't really have side effects and render in, you actually can, but we won't get into that here. We know that putting it in useEffect is awkward, because David said so. But what about outside the component? That's an idea. So where do action effects go? In reality, they go in eventHandlers or sort of. In this example we have an onSubmit and we are submitting the data directly inside that onSubmit. And I'm going to show you the alternative later, but the real mental model is you want to put your fire and forget, your action effects as close as possible to the event that would've caused the effect. And so here's how this helps with React 18, we know that when something changes, our component is going to re-render. And if we tie that effect to rendering, then React, remember it has the ability to just keep re-rendering that component. And because you tied it to rendering, it's going to just keep executing those effects, which we don't really want.
But move the effect closer to the eventHandler, and you avoid that problem completely, because you're not tying it to render, you're tying it to the actual event that cause that effects to happen. So action effects happen outside of rendering.
[10:01] All right, so here's an example which actually goes through quite a few issues and I've seen this before, I've even done this. There's an isFocus prop. And so this isFocus prop makes sure that when we focus the components, so isFocus equals true, we're listening for that state change. And so when that state changes, we are going to execute that useEffect, which is then going to focus the event. So there's a lot of indirection going on here, just in order to get that component to focus. And also there's a possibility of an impossible state. What if you have multiple isFocus is true? In short, isFocus shouldn't really be a prop. But let's see how we could refactor this at least outside of useEffect.
So instead we could use a forward ref and we could just consume that in the components, where we have this inputRef equals useRef. And even though this is a fancy input, we could use forwardRef to grab that. Or if you really want to, use imperative handle. I didn't put it in the slides because I want people screenshotting it and being like, "This guy's recommending use imperative handle, that's too spicy for this talk." But anyway, check it out. We are doing the side effect directly inside of the eventHandler. And so the result is we have a very clean component, we have no useEffects in sight and this is just extremely, extremely simple.
[11:33] So when we think about this though, like executing an effect directly inside the eventHandler, it feels imperative. It feels like when something happens, execute this effect feels a little weird, feels a little side effecty and not really functional, right? But check out the declarative approach where we're saying when something happens causes the state to change and depending on which parts change, this effect should be executed, but only if some condition is true. And React may execute it again for some future reason, but only in strict mode, which you shouldn't disable for some future reason. And I try to hear the explanation for this, I think it's something with offscreen mode or every single time I hear it I lose the reason why, but just don't do it.
Beta React docs
[12:37] Okay, so now we're going to get into the beta React docs. Just checking the time, not much time. Okay, so beta React docs. I'm so thankful that these docs exist by the way. So by the way, if you go to reactjs.org, there's a nice header over here and you could go directly to the beta React docs. Wish we had this three years ago, but it's great that we have it now.
And there's a section in here called You Might Not Need an Effect. So we're going to go over a few of the sections, not all of them, because then this would be a much longer talk. But yeah, let's go through them one by one, especially the ones that are most common to what I see people using useEffect for where they shouldn't.
[13:17] So the first one, you don't need useEffect for transforming data. So here's an example over here where we're calculating the total of a number of items right in useEffect, which makes sense. Whenever the items change, we need to set the total. Guess what? That's a very imperative way of thinking. Instead, what we could do is just put it inside a useMemo, because the total is a derived value and useMemo is very good for derived values. But guess what? You might not even need useMemo. Honestly start without it, and only if you start to notice performance problems should you start to useMemo, I'm saying use a lot. But yeah, much simpler, get rid of the useEffect.
And of course, we know that putting a set state in the useEffect, it could cause a whole bunch of problems anyway, leading people to think, "I will never understand why the default behavior of useEffect is an infinite loop." I've run into this before and the React docs warn you, "You shouldn't do this, this is why." It's because this is going to trigger a re-render, which is going to trigger the effect again, et cetera, et cetera.
You don’t need useEffect for communicating with parents
[14:24] Okay, you don't need useEffect for communicating with parents. This is an interesting one that really highlights the indirection that's going on. So we have onOpen and onClose props. And so whenever we open something here, let's say that this is a product view, we change the state and then depending on whether it's open or closed, we're going to call those props. And we're doing that by putting isOpen in a dependency, which is going to trigger the useEffects.
So you could see here we're clicking states changing then we have an effect, lots of indirection. So instead get rid of the useEffects, put it directly in the eventHandler and now it becomes much more straightforward and easier to understand. So now instead we are just calculating the next state, because we know the next state of that is open and depending on that state directly call on open and on close, and then you could even put it inside of a hook. Sorry.
You don’t need useEffect for subscribing to external stores
[15:28] All right. You don't need useEffect for subscribing to external stores. This one is a really interesting one, because it goes counter to what I was just talking about. It's like useEffect is really good for subscriptions, right? Well, over here I'm subscribing to a store API and all I'm doing is getting some specific value whether I'm connected or not to the store API. And then I have an unsubscribe here. There's still a little bit of indirection here, because we are setting a local state variable.
Guess what? You could actually get rid of this and use the useSync external store hook, which has three parts, a subscribe part where you tell it, "Here's how to subscribe, here's a function you need to call a subscribe." The get snapshot part, which is, "This is the value that I want to get and here's how to read it from that store." And a server snapshot where it's just good for SSR. So you put all those three things together, it's going to automatically subscribe and get you the value you need. No use state, no useEffect. It's a really brilliant hook. So I do recommend that you at least try it out. They say it's only for library authors, but they also said like, "Do not use or you will be fired. It's only for React maintainers." But we're seeing libraries use it for whatever reason, like preact signal and stuff like that. So go ahead and use it. Not that one, but useSync external store.
You don’t need useEffect for fetching data
[16:51] All right, this one is a big one. You don't need useEffect for fetching data. Like we talked about at the beginning, that was one of the big reasons that we used useEffect. So instead of all of this that we were doing before, just use the framework. Whatever you're using, you're likely using a framework. Remix has a really nice loader over here, next.js has getServerSideProps. And so this is going to be available to you directly in the component. And then there's a library that hopefully most of you are using for fetching data, which is React Query. But notice over here I'm doing something different. I'm doing queryClient.prefetchQuery, because the idea is that we want to start fetching as soon as possible, which is the subject of a whole other talk. But so yeah, instead of useEffect, really useQuery, next.js has useSWR or just use, right?
This is going to happen in the future if you're not familiar with the use hook, it basically acts the same. Or this is going to be the new upcoming way of doing suspense, I think. It's an RFC, React might kill it. They like doing the whole Google thing where they kill stuff. Just kidding, they've shipped a lot of really great stuff.
[18:09] But the important thing is this cache over here. This cache means that whenever we try to read this, we are not going to keep re-fetching and re-fetching. I guarantee you, you're going to see React tutorials where they forget about this whole cache part or they start fetching in here, you're going to get waterfalls again. So just make sure fetch as early as possible and cache.
And Dan talks about a lot of problems with fetching and useEffect. First of all, the race conditions that we talked about, there's no instant back button. You're going to see loading spinners everywhere when you try to go back, because components are going to try to load again. No initial HTML content, so you're going to have even more loading spinners and you're chasing waterfalls, because the parent's going to unload, and then when that's unloading, the child's going to load when that's unloading, et cetera, et cetera. And so you want to avoid those things.
You don’t need useEffect for initializing global singletons
[19:00] Okay, so another thing, you don't need useEffect for initializing global singleton. So here's what I mean. Let's say that we've done this a lot, where we execute some sort of effect when our app starts. And so we're like useEffect is the right place for effects, right?
So we're calling the store API.authenticate, but we know that in React 18 this is going to run twice. So instead we use a ref and we say, "Hey, did this already run?" Maybe not, but this is honestly a red flag, so I recommend you not do it. So instead of using a ref, we could just stick that outside. But guess what? Why don't we just stick the entire thing outside? Look how simple that is. A lot of you might be internally screaming. This is something that Dan recommended on Twitter, so I feel okay putting this on the screen. But if you're using next, you could check if type of … was undefined or not and call it conditionally like that. And then if you're going to scream about testing, then just wrap it in a function and then you could even inject that store API, do whatever you want to do, just do it outside of the component.
You don’t need useEffect for handling user events
[20:10] And then the biggest one for me, you don't need useEffects for handling user events. And so this goes to activity effects versus action effects. So now we're talking about action effects.
So in this one we are submitting a form and we're setting all of these variables. So setIsLoading is true, set the form data to the event. And so that's going to be indirect and trigger a change. This is wrong. What you should do is move it inside of the eventHandler. So move it inside of submit and then you might need to add a whole bunch of extra logic, but because you're not using useEffect and you're isolating it to that eventHandler, you could easily abstract that to a hook. My favorite type of hook looks like this, where you have a state and a send and all you have to do in your components is send events. This is useReducer, this is XStates, the library I maintain, this is Redux, Zustand this works the same way.
[21:09] So here is a bigger example. I wanted to just make this cool video, where it's a thumbnail, but when you click it, it goes full screen. And then there's many ways to close it. You could click outside of it, you could press the escape key or you could wait for the video to be over. There's three useEffects that are handling this. So the first one we are checking isPlaying. And so if we are playing the video ref is we have to play it or we have to pause it, if it's not. The second one, we are listening for the video to end. And if it's not ending, then we have to set isPlaying to false. And remember, this is going to trigger some indirection, it's going to trigger the state's change, and then the state is going to trigger the previous effect. And same thing over here, when we press the escape key, again, we're setting Is playing to false. And now we have a little bit of cleanup too.
So the way I like to think about these instead, in terms of declarative effects is by using a state machine. And no, I'm not going to talk about exceeding this talk, but you could talk to me afterwards about it. So with state machines, you declare the effects that are going to execute directly on the transition. And I think this is brilliantly simple. So for example, when we're in mini and we transition to full, we're doing it on a toggle and we play the video. Same thing when we're going from full to mini, we go from toggle and we're pausing the video and then we do the transition. So if you haven't used XState, just imagine this as Redux or your standard reducer. Imagine if you could execute effects directly inside of your reducer. It's basically the same idea.
[22:46] And so I really like this, because it really gives you a visual way and a declarative way of thinking about all of your effects without having to just tangle your mind and think, "Okay, when is this effect going to execute? I have absolutely no idea." So yeah, this is just my way of telling you, start looking into I guess state machines, because state machines are purpose built for declaratively expressing all of these types of effects.
[23:16] So in summary, or not summary, but where do action effects go? Not really eventHandlers, but technically they do go in state transitions which happen to be executed at around the same time as eventHandlers. Now I don't have much time, so let's wrap up with a summary.
UseEffect is for synchronization.
Activity effects go in useEffect.
Action effects go in eventHandlers.
You should render as you fetch.
And last but not least, state transitions trigger effects. So with that said, I want to thank you all very much.
[23:55] Eli Schutze: Amazing. Please, step into my office. Thank you so much, David. That was fascinating and also very funny, which I appreciate. Remember you can ask questions on the sli.do, S-L-I.D-O. The code is 2124. As per usual, the people want to know what presentations software are you using. Because it's beautiful.
[23:16] David Khourshid: So this is slides.com, because I'm too easy to learn. Keynote. And also slides.com is super easy, plus you could customize it with CSS. They have that magic move thing too. It's really Nice.
[23:16] Eli Schutze: And you can present offline. That's what I use [inaudible 00:24:36]. Yeah.
David Khourshid: Oh yes, yep.
Eli Schutze: Let's talk about effects. I love you might not need concept, because I'm a simple gal. I don't like to over-complicate things. But if I wanted to take all of this into say, the real world, a team, a job, are there any tools we could use, some linting, some notes, a blog post, any resource in order to ensure we're conscious of these things?
[23:16] David Khourshid: Good question. I mean, I guess you could watch my talks, but then they're going to be like, "Oh, this is just David blabbing on about useEffects." So I'll write a blog post about it. The official Reacts docs, the beta docs honestly, stop reading the old docs, read the beta docs, those are going to be the resource for basically everything I talked about during this talk. And they even have a couple more examples of where you should not use a useEffect. So definitely read through that. As far as linting rules, just let React 18 make your app crash in developments and be like, "Oh, why did that happen?" Probably a useEffect.
[23:16] Eli Schutze: I feel like I know the answer to this, but what are your thoughts on that lint rule, that's don't put functions in your eventHandlers?
[23:16] David Khourshid: How about don't add lint rules until they actually become a problem, how's that?
[23:16] Eli Schutze: That's great. Cheers for that. All right, we have a few questions. All right, fetch as your render sounds good until you depend on arguments for your request. What is the best approach then?
[23:16] David Khourshid: Well, think about when those arguments happen. The user probably initially yates it or it's coming from an outside source. So for example, the user might press a button, might fill out a form, and then when they submit a form that point where they submit a form, that's when you should be initiating the request. What a lot of React developers do is they'll submit a form that sets a state or that gets passed down as a prop to a component. And then the component's like, "Oh hey, I have a prop. Now I can read that prop in fetch data based on that prop." But there is a lot of in interaction there. So start fetching as early as possible. You're going to learn that React rendering is not an event or it's actually not the events that you should be caring about. The user actually doing stuff or some external thing doing stuff, that's what you have to focus on.
[23:16] Eli Schutze: There's a lot of questions and I'm going to bundle them together about all the alternatives. So isn't useReduce good for fetching? Or do you think we're going to use Redux again? Or when would you use useEffect without a dependency array? What about use event? All the uses.
David Khourshid: So use events, dead. React likes killing stuff.
Eli Schutze: The Liz Truss effect.
David Khourshid: Yeah. What were the other alternatives?
Eli Schutze: Reducer.
[23:16] David Khourshid: Things like, all right, useReducer, redux, et cetera, these are state containers. They're not really ways that you would fetch effects. But like I mentioned with state machines, you should be thinking about when your state changes, you should have some way of indicating this is a new state and these are the effects that are supposed to happen. A lot of times when we think about it, we're like, "here's a new state. And then we have some middleware that runs where it's like, "Okay, maybe I'll fetch." But I think that both of those should go together.
[23:16] Eli Schutze: Thank you, David. What tool did you use to visualize the state machine in your slide?
[23:16] David Khourshid: Great question. Not a planted question, I promise. It is a Stately.AI/editor. It's free to use, play around with it.
Eli Schutze: It's really pretty.
David Khourshid: Yeah.
[23:16] Eli Schutze: Okay. One more. Oh, actually maybe two more. Did you ever think about contributing to the React docs?
[23:16] David Khourshid: Yes. So I did contribute to the React docs once, because they had use timeout MS. It was for some very, very old suspense API, but I was like, "Wait a minute, it just says use timeouts in the docs. I could add that little MS to the side." And that was my first React contribution, because their actual source code might as well be closed source. There's bit shifting in all these weird fiber stuff and I'm like, "That is very unapproachable for any dev who does not work at Meta and probably most devs who work at Meta." So yeah.
[23:16] Eli Schutze: I think that's fair. And then finally, great talk, are your slides now or later going to be posted online somewhere that we can [inaudible 00:28:58].
David Khourshid: Yeah, of course.
Eli Schutze: Cool. Follow David on Twitter at davidkpiano.
David Khourshid: davidkpiano.
Eli Schutze: Thank you, David.
David Khourshid: Thank you.
Eli Schutze: Round of applause, please for David.