Understanding useEffect Hook

Hashedin

Tejas Upmanyu

25 May 2020

When React Hooks were released at the React Conference 2018, they took the React community all across the globe by storm. Finally, there was a refreshing and fundamental change in the way everyone thinks about and writes React code. One of the attractive (yet mysterious?) aspects of using hooks was the brevity they imparted to projects. As everyone was pumped up about giving these all-new hooks a spin, so was I. Sure, useState, useMemo and useContext look neat and are fairly simple to understand. But there, in plain sight is probably one of the most critical hooks – useEffect.

 

useEffect is supposed to be this all-in-one replacement for a bunch of old lifecycle methods like componentDidMount, componentDidUpdate to name a few, and hence getting the right understanding of useEffect hook and hooks, in general, is paramount to writing consistent, clean and bug-free data-driven React applications.

 

Here are some of the questions you might be having after employing the useEffect hook, which we’ll seek answers to 

 

How to emulate componentDidMount with useEffect?

How to emulate componentDidUpdate with useEffect?

How to access previousProps as we did in componentDidUpdate with useEffect?

How to stop an infinite refetching loop?

Should functions be specified as dependencies to useEffect?

In case you are looking for answers to these questions real quick, check out the TLDR; section at the end.

 

Understanding Rendering

Functional components differ from traditional class components in a prominent way and that is the way they close around state and props. In more simplified terms, each ‘render’ owns a separate set of state and props. Now that might not be that simple to understand. Let’s go with a simple example –

 


function Counter() {

  const [count, setCount] = useState(0)
  return (

    <div>

      <p>You clicked {count} times</p>

      <button onClick={() => setCount(count + 1)}>Click me</button>

    </div>

  )

}



Every time user clicks on the button, setCount updates the state which triggers a re-render with the new value. Initially, the count is 0, and as the first click happens on button state updates via setCount, calling the Counter function again with a new value of count which would be 1. For the second render, Counter sees its individual value for the count. So the count is constant for each render and Its value is isolated between different renders. This is not only true for state values but also for props, events handlers, functions, and pretty much everything. Here is another example to clarify this –

 


function Greeting() {

  const [name, setName] = useState("")

  function handleGreetClick() {

    setTimeout(() => {

      alert("Hey " + name)

    }, 5000)

  }

  return (

    <div>

      <p>Name is {name}</p>

      <input type="text" onChange={event => setName(event.target.value)} />

      <button onClick={handleGreetClick}> Greet </button>{" "}

    </div>

  )

}



Okay, let’s say we type ‘Dan’ in the input and click greet button, then we enter ‘Sunil’ and click greet button before 5 seconds. What message will the greeting alert show? will it be ‘Hey Dan’ or ‘Hey Sunil’? It is the latter one right? No. Here’s the sandbox for you to give it a spin and see the difference between class components and functional components.

 

So why is it so? We’ve discussed this above. Each render owns the state for functional components which is not true for class components. Next time someone asks you the difference between class and functional components, do remember this.

 

Although, you should totally follow Dan and Sunil on Twitter!

 

Each render owns its State, Props, Effects …Everything.

As we saw above, each render owns its state, props, and pretty much everything including effects.

 


function Greeting() {

  const [name, setName] = useState("")


  React.useEffect(() => {

    document.title = `Hey ${name}`

  })

  return (

    <div>

      <p>Name is {name}</p>

      <input type="text" onChange={event => setName(event.target.value)} />

    </div>

  )

}



In this example above, as the user types a name in the input field, setName gets triggered and sets the new name in the state, leading to a re-render. How does the effect know about the latest value of a name? Is it some sort of data-binding? Nope. We learned earlier that count is different and isolated for each render. Functions, event handlers, and even effects can ‘see’ values from the render they belong to. So the correct mental model to paint is – It is not only a changing name inside an unchanging effect. It is the effect that also is different for each render.

 

Let’s set this straight. Every function inside a component render sees the state, props particular to that render only.

 

So, the eventual question in your mind would be – How can I access state and prop from next or previous renders, just like class components. Well, it would be going against the flow, but it is definitely possible using refs. Be aware that when you want to read the future props or state from a function in a past render, you’re intentionally going against the flow. It’s not wrong (and in some cases necessary) but it might look less “clean” to break out and that’s intended to highlight which code is fragile and depends on timing. In classes, it’s less obvious when this happens, which can lead to strange bugs in some cases.

 


function Greeting() {

  const [name, setName] = useState("")

  const latestName = useRef()

  useEffect(() => {

    // Set the mutable latest value

    latestCount.current = name

    setTimeout(() => {

      // Read the mutable latest value

      console.log(`You entered ${latestName}`)

    }, 5000)

  })

  return (

    <div>

      <p>Name is {name}</p>

      <input type="text" onChange={event => setName(event.target.value)} />

    </div>

  )

}

This code will always log the latest value of name, behaving like classes. So it might feel a bit strange but React mutates this.state the same way in classes.

 

What about the ‘cleanup’ Function?

Going through the docs, you will find mentions of a certain cleanup function. The cleanup function does exactly as it says – cleans up an effect. Where will it be required you might ask, well any cleanup function makes sense in cases where you have to unsubscribe from subscriptions or say remove Event Listeners.

 

Many of us have a slightly wrong mental model regarding the working of cleanup functions. borrowing the example from React docs;

 


useEffect(() => {

  ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange)

  return () => {

    ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange)

  }

})

if props.id is 1 on first render and 2 on second. How will this effect work ?

subscribe with id = 1

unsubscribe with id = 1

then

subscribe with id = 2

unsubscribe with id = 2



That is the synchronized model, we derive from our understanding of classes. Well, it doesn’t quite stand true here. As you are assuming that cleanup ‘sees’ the old props because it runs before the next render and that’s now how it happens. We have to understand that React runs an effect only after the browser has painted, making apps feel faster as effects no longer block the painting, so why should cleanup function block the next render? Thankfully it doesn’t. The previous effect is cleaned up after the re-render with new props

 

You must be a little uncomfortable with this. How can cleanup function still see ‘old’ props once the next render has happened? Well, reiterating our learnings from above –

 

EVERY FUNCTION INSIDE A COMPONENT, INCLUDING EFFECTS AND TIMEOUTS, CAPTURES THE STATE AND PROPS OF THE RENDER.

 

So no ‘new’ props and state are available to the cleanup function but the ones available to other functions and effects in that render and hence cleanups don’t require to be run right before next render, they can run after the next render as well.

 

Telling React When to Run an Effect

This is pretty much on the lines of how React works, it learned the lesson that instead of changing the whole DOM on every change, only update the parts that actually need updating. There are times when running an effect is not necessary, so how do you instruct React on whether to run this effect or not?

 


function Greeting({ name }) {

  const [counter, setCounter] = useState(0)

  const [name, setName] = useState("Tejas")

  useEffect(() => {

    document.title = `Hello, ${name}`

  })
  return (

    <h1 className="Greeting">

      Hello, {name}

      <button onClick={() => setCounter(count + 1)}> Increment </button>{" "}

    </h1>

  )

}

In this example, when you click the increment button, the state updates, and the effect fires, unnecessarily even though the name it uses hasn’t changed. So probably React should get a diff between old effect and new effect to see if anything has changed? No, that can’t happen as well because React can’t magically tell the difference in two functions without executing them, which defeats the purpose, right?

 

To get out of this problem, useEffect hook has a provision for providing a second argument – An array of dependencies. An array of dependencies or ‘deps’ signify what are the dependencies for that effect. If the dependencies are the same between renders, React understands that the effect can be skipped.

 

Being Honest About Effect Dependencies

Dependency array or ‘deps’ are critical to bug-free usage of useEffect. Giving incomplete or wrong dependencies can certainly lead to bugs. The statement to internalize here is –

 

ALL VALUES FROM INSIDE YOUR COMPONENT, USED BY THE EFFECT MUST BE SPECIFIED IN THE DEPENDENCY ARRAY.

This is of great relevance and we as developers often lie to React about dependencies of an effect. Often unintentionally, owing to our reliance on the class lifecycle model where we still try to emulate ComponentDidMount or ComponentDidUpdate. We need to unlearn that paradigm or just switch it off before using hooks, because as we have seen so far – Hooks are quite different from classes and carrying that baggage of class model onto hooks can lead to inaccurate mental models and formation of wrong concepts which can be hard to shake.

 

Being honest about dependencies is quite simple if you follow these two conventions –

 

Include all the values inside the component that are used inside the effect.

 

OR

 

Change the effect code, so that you don’t need to specify dependency anymore.

The mental model of hooks generated by our usage of classes sways us against considering functions as part of data flow and hence a dependency on an effect, which is not true. In reality, Classes obstruct us from considering functions as part of the data flow and that can be understood better by this example –

 


class Parent extends Component {

  state = {

    query: "react",

  }

  fetchData = () => {

    const url = "https://hn.algolia.com/api/v1/search?query=" + this.state.query

    // ... Fetch data and do something ...

  }

  render() {

    return <Child fetchData={this.fetchData} />

  }

}

 

class Child extends Component {

  state = {

    data: null,

  }

  componentDidMount() {

    this.props.fetchData()

  }

  render() {

    // ...

  }

}

now if we want to refetch in the child based on changes in query, we cannot just compare fetchData between renders because it is a class property and will always be the same, which will land us in infinite refetching condition. So we have to take another step unnecessarily – passing query as a prop to Child and comparing the value of query in componentDidUpdate between renders.

 


class Child extends Component {

  state = {

    data: null,

  }

  componentDidMount() {

    this.props.fetchData()

  }

  componentDidUpdate(prevProps) {

    //  This condition will never be true as fetchData is a class property and will always remain same

    if (this.props.fetchData !== prevProps.fetchData) {

      this.props.fetchData()

    }

  }

}



Here you can clearly see how classes break out and do not allow us to have functioned as part of data flow but hooks do because with hooks you can specify functions as a dependency hence making them part of the data flow.

 


function Parent() {

  const [query, setQuery] = React.useState("react")

 

  const fetchData = useCallback(() => {

    const url = "https://hn.algolia.com/api/v1/search?query=" + query

  }, [query])

 

  return <Child fetchData={fetchData} />

}

 

function Child({ fetchData }) {

  let [data, setData] = useState(null)

 

  useEffect(() => {

    fetchData().then(setData)

  }, [fetchData]) // Effect deps are OK

 

  // ...

}



✅ Though there is one minor catch here, which you must understand. If we hadn’t wrapped fetchData in a useCallback with a query as a dependency, fetchData would change on every render, which when supplied as a dependency to useEffect in child component would needlessly trigger the effect over and over again. Not ideal, huh.

 

useCallback allows functions to fully participate in the data flow. Whenever function inputs or dependencies change, the output function changes, else it remains the same.

 

More importantly, passing a lot of callbacks wrapped in useCallback isn’t the best of choices and can be avoided. As React docs say –

 

We recommend to pass dispatch down in context rather than individual callbacks in props. The approach below is only mentioned here for completeness and as an escape hatch. Also, note that this pattern might cause problems in the concurrent mode. We plan to provide more ergonomic alternatives in the future, but the safest solution right now is to always invalidate the callback if some value it depends on changes.

 

TLDR

As discussed in the introduction, here are my answers from the understanding of useEffect hook.

 

How to emulate componentDidMount with useEffect?

It’s not an exact equivalent of componentDidMount because of the differences in class and hook model but useEffect(fn, []) will cut it. As told over and over again, useEffect captures the state and props of the render it is in, so you might have to put in some extra efforts to get the latest value, i.e useRef. Although, try to come to terms with thinking in terms of effects rather than mapping concepts from class lifecycles to hooks.

 

How to emulate componentDidUpdate with useEffect?

Again not exactly emulating componentDidUpdate and I do not encourage thinking of hooks with the same philosophy as of class lifecycle methods but useEffect(fn, [deps]) is a replacement. You do need to specify correct dependencies in the array, omitting or adding dependencies might result in bugs. Though to access previous or latest props, employ useRef.

 

How to access previousProps as we did in componentDidUpdate with useEffect?

To access previous or latest props is to break out of paradigm and is a little more effort, employ useRef to keep track of previous or latest values for state and props. usePrevious described here is a widely used custom hook for the job.

 

How to stop an infinite refetching loop?

Data fetching in useEffect is a routine use case and while useEffect is not exactly made for the job. (Waiting for Suspense to become production-ready 💓) – Meanwhile, this guide covers how to fetch data properly using hooks. You enter infinite refetching land usually when you don’t specify second argument (deps) for the effect which leads to the effect being triggered over and over. Give a proper set of dependencies to useEffect to avoid infinite refetching problem.

 

Should functions be specified as dependencies to useEffect?

Yes, absolutely. If your functions use state/prop do wrap them in a useCallback as we saw above. Functions relying on state/props should be part of data flow using hooks, otherwise, you can just hoist them outside your component.

 

Closing Notes

This post is an attempt to make useEffect more understandable and to document my understanding of useEffect. Most of the examples and inspiration come from the exhaustive and Complete guide on useEffect by Dan Abramov, which you should definitely go through. I thank him for teaching me and to you, for taking the time to read this post.

 


Have a question?

Need Technology advice?

Connect

+1 669 253 9011

contact@hashedin.com

linkedIn youtube