--- { title: "Rules of React's useEffect", description: "useEffect is prolific in React apps. Here are four rules associated with the hook and in-depth explanations of why they're important.", published: '2022-02-22T22:12:03.284Z', authors: ['crutchcorn'], tags: ['react', 'javascript'], attached: [], license: 'coderpad', originalLink: 'https://coderpad.io/blog/development/rules-of-reacts-useeffect/' } --- React’s `useEffect` is a powerful API with lots of capabilities, and therefore flexibility. Unfortunately, this flexibility often leads to abuse and misuse, which can greatly damage an app’s stability. The good news is that if you follow a set of rules designated to protect you during coding, your application can be secure and performant. No, we’re not talking about React’s “[Rules of Hooks](https://reactjs.org/docs/hooks-rules.html)”, which includes rules such as: - No conditionally calling hooks - Only calling hooks inside of hooks or component - Always having items inside of the dependency array These rules are good, but can be detected automatically with linting rules. It's good that they're there (and maintained by Meta), but overall, we can pretend like everyone has them fixed because their IDE should throw a warning. Specifically, I want to talk about the rules that can only be caught during manual code review processes: - Keep all side effects inside `useEffect` - Properly clean up side effects - Don't use `ref` in `useEffect` - Don't use `[]` as a guarantee that something only happens once While these rules may seem obvious at first, we'll be taking a deep dive into the "why" of each. As a result, you may learn something about how React works under the hood - even if you're a React pro. ## Keep all side effects inside `useEffect` For anyone familiar with React’s docs, you’ll know that this rule has been repeated over and over again. But why? Why is this a rule? After all, what would prevent you from storing logic inside of a `useMemo` and simply having an empty dependency array to prevent it from running more than once? Let’s try that out by running a network request inside of a `useMemo`: ```jsx const EffectComp = () => { const [activity, setActivity] = React.useState(null); const effectFn = React.useMemo(() => { // Make a network request here fetch("https://www.boredapi.com/api/activity") .then(res => res.json()) .then(res => setActivity(res.activity)); }, []) return
{activity}
} ``` Huh. It works first try without any immediately noticeable downsides. This works because `fetch` is asynchronous, meaning that it doesn’t block the [event loop](https://www.youtube.com/watch?v=8aGhZQkoFbQ&vl=en). Instead, let’s change that code to be a synchronous `XHR` request and see if that works too. ``` function getActivity() { var request = new XMLHttpRequest(); request.open('GET', 'https://www.boredapi.com/api/activity', false); // `false` makes the request synchronous request.send(null); return JSON.parse(request.responseText); } const EffectComp = () => { const [data, setData] = React.useState(null); const effectFn = React.useMemo(() => { setData(getActivity().activity); }, []); returnHello, world! {data}
; } ``` Here, we can see behavior that we might not expect to see. When using useMemo alongside a blocking method, the entire screen will halt before drawing anything. The initial paint is then made after the fetch is finally finished. However, if we use useEffect instead, this does not occur. Here, we can see the initial paint occur, drawing the “Hello” message before the blocking network call is made. Why does this happen? ### Understanding hook lifecycles The reason `useEffect` is still able to paint but useMemo cannot is because of the timings of each of these hooks. You can think of `useMemo` as occurring right in line with the rest of your render code. In terms of timings, the two pieces of code are very similar: ```jsx const EffectComp = () => { const [data, setData] = React.useState(null); const effectFn = React.useMemo(() => { setData(getActivity().activity); }, []); returnHello, world! {data}
; } const EffectComp = () => { const [data, setData] = React.useState(null); setData(getActivity().activity); returnHello, world! {data}
; } ``` This inlining behavior occurs because `useMemo` runs during the “render” phase of a component. `useEffect`, on the other hand, runs **after** a component renders out, which allows an initial render before the blocking behavior halts things for us. Those among you that know of “useLayoutEffect” may think you have found a gotcha in what I just said. “Ahh, but wouldn’t useLayoutEffect also prevent the browser from drawing until the network call is completed?” Not quite! You see, while useMemo runs during the render phase, useLayoutEffect runs during the “*commit”* phase and therefore renders the initial contents to screen first. > [useLayoutEffect’s signature is identical to useEffect, but it fires synchronously after all DOM mutations.](https://reactjs.org/docs/hooks-reference.html#uselayouteffect) See, the commit phase is the part of a component’s lifecycle *after* React is done asking all the components what they want the UI to look like, has done all the diffing, and is ready to update the DOM.  > If you’d like to learn more about how React does its UI diffing and what this process all looks like under the hood, take a look at [Dan Abramov’s wonderful “React as a UI Runtime” post](https://overreacted.io/react-as-a-ui-runtime/). > > There’s also [this awesome chart demonstrating how all of the hooks tie in together](https://github.com/Wavez/react-hooks-lifecycle) that our chart is a simplified version of. Now, this isn’t to say that you should optimize your code to work effectively with blocking network calls. After all, while `useEffect` allows you to render your code, a blocking network request still puts you in the uncomfortable position of your user being unable to interact with your page. Because JavaScript is single-threaded, a blocking function will prevent user interaction from being processed in the event loop. > If you read the last sentence and are scratching your head, you’re not alone. The idea of JavaScript being single-threaded, what an “event loop” is, and what “blocking” means are all quite confusing at first. > > We suggest taking a look at [this great explainer talk from Philip Robers](https://www.youtube.com/watch?v=8aGhZQkoFbQ) to understand more. That said, this isn’t the only scenario where the differences between `useMemo` and `useEffect` cause misbehavior with side effects. Effectively, they’re two different tools with different usages and attempting to merge them often breaks things. Attempting to use `useMemo` in place of `useEffect` leads to scenarios that can introduce bugs, and it may not be obvious what’s going wrong at first. After long enough, with enough of these floating about in your application, it’s sort of “death by a thousand paper-cuts”. These papercuts aren't the only problem, however. After all, the APIs for useEffect and useMemo are not the same. This incongruity between APIs is especially pronounced for network requests because a key feature is missing from the `useMemo` API: effect cleanup. ## Always clean up your side effects Occasionally, when using `useEffect`, you may be left with something that requires cleanup. A classic example of this might be a network call. Say you have an application to give bored users an activity to do at home. Let’s use a network request that retrieves an activity from an API: ```jsx const EffectComp = () => { const [activity, setActivity] = React.useState(null); React.useEffect(() => { fetch("https://www.boredapi.com/api/activity") .then(res => res.json()) .then(res => setActivity(res.activity)); }, []) return{activity}
} ``` While this works for a single activity, what happens when the user completes the activity? Let’s give them a button to rotate between new activities and include a count of how many times the user has requested an activity. ```jsx const EffectComp = () => { const [activity, setActivity] = React.useState(null); const [num, setNum] = React.useState(1); React.useEffect(() => { // Make a network request here fetch("https://www.boredapi.com/api/activity") .then(res => res.json()) .then(res => setActivity(res.activity)); // Re-run this effect when `num` is updated during render }, [num]) return (You should: {activity}
You have done {num} activities
You should: {activity}
You have done {num} activities
{count}
{count}
{shouldRender && }{count}
{shouldRender && }