Concepts that will make you a pro at useEffect hook

ยท

7 min read

In this blog, I'll be going over the most common mistakes that are made by even experienced ReactJS developers while using useEffect hook and some core concepts that will make you a pro in useEffect hook!

but first, let me give you a little refresher as to what useEffect is

I'll strongly advise to have a codesandbox window open so that you can run the code and actually see the errors and how they are being solved!

What is the useEffect hook?

Ever since the introduction of functional-based components, useEffect has taken over the function of the class-based component lifecycle by performing the function of componentDidMount, componentDidUpdate and componentDidUnmount on its own. If you've used ReactJS, you would have come across the useEffect hook and used it to render the updated DOM as any of the states are updated, or fetch the data from API when the page loads.

Let's take a very simple example of how the useEffect functions through this piece of code

import "./styles.css";
import { useState, useEffect } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  function handleClick() {
    // Make sure to never update your state directly to avoid bugs
    setCount((prev) => prev + 1);   
}

  useEffect(() => {
    document.title = `button ${count}`;
  }, [count]);

  return (
    <div className="App">
      <button onClick={handleClick}>Increase</button>
      <h1>The button was pressed {count} times</h1>
    </div>
  );
}

Now, if you run the above code, you'll observe a strange phenomenon, there is a slight delay in updating the page title. Now, I experienced this problem while I was working on the Pomodoro feature in my chrome extension and It took me days to figure out how this works!

To understand this simply, I'll give you an analogy, Whenever there is a change in the state of a component, React updates the DOM, and only after the update has been rendered on the DOM, does the useEffect run, hence, in simple words, useEffect runs after the update are rendered and not simultaneously.

What is a dependency array?

useEffect, by default, will run on every re-render, to make sure we are only running the useEffect when a particular state is changed, we will be using the dependency array. This concept is very useful when there are multiple states in our component, let's take an example,

export default function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("Utkarsh");

  function handleClick() {
    setCount((prev) => prev + 1);
  }

  useEffect(() => {
    document.title = `button ${count}`;
    console.log("UseEffect ran");
  });

  return (
    <div className="App">
      <button onClick={handleClick}>Increase</button>
      <h1>The button was pressed {count} times</h1>
      <input type="text" onChange={(e) => setName(e.target.value)} />
//You will observe that even though we are updating the name state, 
//useEffect is also running
    </div>
  );
}

This can be especially troublesome if you are doing heavy tasks like fetching data from an API, to solve that we can simply use a dependency array to make sure the effect only runs when the count state is updated, now our useEffect will look like this

  useEffect(() => {
    document.title = `button ${count}`;
    console.log("UseEffect ran");
  },[count]);

Common mistake with dependency array

This was something that I learnt the hard way in my chrome extension project!

Using useEffect with Non-Primitive data types as a dependency: I stored all the details as a single state object to avoid having multiple states, but when it came to updating run the useEffect, it was all chaos. Let's understand this with an example where we have an object with two values name:string and selected:boolean

export default function App() {
  const [name, setName] = useState("");
  const [state, setState] = useState({
    selected: false,
    name: ""
  });

/*
Whenever the state is updated for the first time, it's okay
but when we click on the button without any change in value
We observe that still, the useEffect is running
But why?
*/
  const handleAdd = () => {
    setState((prev) => ({ ...prev, name }));
  };

  const handleSelect = () => {
    setState((prev) => ({ ...prev, selected: true }));
  };

  useEffect(() => {
    console.log("UseEffect ran");
  }, [state]);

  return (
    <div className="App">
      <input type="text" onChange={(e) => setName(e.target.value)} />
      <button onClick={handleAdd}>Add Name</button>
      <button onClick={handleSelect}>Select</button>
      <h1>{`name:${state.name}
       selected:${state.selected}`}</h1>
    </div>
  );
}

The answer lies in how JavaScript compares primitive and non-primitive data types. Primitive datatypes include strings, numbers and booleans whereas non-primitive datatypes include arrays and objects. Whenever we perform comparison operations on primitive data, their values are compared i.e

a = "Hello"
b = "Hello"
a===b //True

n1 = 1
n2 = 1
n1 === n2 //True

But things take a different turn when it's time to compare non-primitive data types

obj1 = {name:"Luffy"}
obj2 = {name:"Luffy"}

obj1 === obj2 //true right?
// It gives false!

a1 = []
a2 = []
a1 == a2 //Again false!

This is because JavaScript compares the memory address of non-primitive datatypes when asked to check for equality rather than comparing their values. It's like two different shopping carts having the same items.

So how to prevent updating the DOM when no change is done, It's simple! you can directly add the exact values in the dependency array, which will make the useEffect look something like this

  useEffect(() => {
    console.log("UseEffect ran");
  }, [state.name, state.selected]);

//Before it was
  useEffect(() => {
    console.log("UseEffect ran");
  }, [state]); //As it checked the equality for the whole object, it always 
// saw the change and hence executed

Not using cleanup function properly If you've tried to make a simple timer in React you would have come across this error, trust me I almost abandoned my project once because of this. Let's understand it with an example

export default function App() {
  const [timer, setTimer] = useState(0);

  useEffect(() => {
    console.log("UseEffect ran");
    setInterval(() => {
      setTimer((prev) => prev + 1);
    }, 1000);
  }, [timer]);

  return <div className="App">{timer}</div>;
}

Seems, okay right? Try to execute this and you will notice a weird glitch. If you closely observe, we are stuck on an infinite loop, and in each loop we are setting a new setInterval without cleaning up the previous one. So how to solve this? This is pretty simple, we use the cleanup function to clear the previous interval as soon as the update is rendered on the screen like this

export default function App() {
  const [timer, setTimer] = useState(0);

  useEffect(() => {
    console.log("UseEffect ran");
    const tick = setInterval(() => {
      setTimer((prev) => prev + 1);
    }, 1000);
    return ()=>{
      clearInterval(tick)
    }
  }, [timer]);

  return <div className="App">{timer}</div>;
}

Correct way to fetch data from an API with useEffect Hook

Now it's a very common practice to take the help of useEffect to fetch data when the page loads to render on the screen.

Now when I first started using API data, I never looked into the possibility that there can be serious bugs in just fetching data. For this imagine a use case, the API provides us with the ability to fetch the data about two users, on the click of a button for each, click on user 1 and the first user's data will be fetched and rendered and similarly click on the latter for the same.

Now imagine if our network is slow and we quickly clicked on the first button and then clicked on the second button. What will happen? The data for the first user will be fetched, the state will be updated then the second user's data will be fetched and the state will be updated again and rendered. Now just imagine, if there was huge data as a response from the API.

We would be requiring something to intercept this fetch in between and cancel it if needed, the easiest way is like this

useEffect(() => {
    var isCancelled = false;
    fetch("https://jsonplaceholder.typicode.com/posts")
      .then((res) => res.json())
      .then((data) => {
        if (!isCancelled) {
          setPosts(data);
          console.log(posts);
        }
      });

    return () => {
      isCancelled = true;
    };
  }, []);

The above method works perfectly fine, however if you want a more professional way we can use AbortController()

  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;
    fetch("https://jsonplaceholder.typicode.com/posts", signal)
      .then((res) => res.json())
      .then((data) => {
        setPosts(data);
        console.log(posts);
      })
      .catch((err) => {
        if (err.name === "AbortError") {
          console.log("Request cancelled");
        } else {
          console.err(err);
        }
      });

    return () => {
      controller.abort();
    };
  }, []);

Closing words

Well, that was a pretty long article! Hope it will add value to your journey as a react developer!๐Ÿ˜Š

ย