DEV Community

Cover image for Optimizing useEffect in React
Huzaima Khan
Huzaima Khan

Posted on • Originally published at huzaima.io

Optimizing useEffect in React

Originally published at https://huzaima.io.

useEffect is one of the most widely used hooks in React. Optimizing useEffect in React gives you a significant boost in performance and sometimes gets your code rid of nasty bugs. I've encountered many situations where there were performance issues or some difficult-to-find bugs due to incorrect usage of useEffect. Here I'll discuss a few of the optimization patterns which are very easy to adhere to yet very effective.

1. Use primitives in the dependency array

We often pass JavaScript objects in the useEffect dependency array. Although nothing is wrong with doing that, sometimes it makes more sense to destructure the JavaScript object and only pass the specific value to the dependency array of useEffect. Let's take a look at an example. Suppose we have a UserInfo component which takes the user object as a prop and fetches the user's transaction data whenever the user changes. We need to only get the user's transaction data when the user changes. Our user object looks like this:

{
  "id": 123,
  "name": "John Doe",
  "imageURL": "...",
  "isActive": false
}
Enter fullscreen mode Exit fullscreen mode

The typical implementation will look like this:

function UserInfo({ user }) {

  useEffect(() => {
    getUserData(user.id);
  }, [user]);

  return (
    ...
  );
}
Enter fullscreen mode Exit fullscreen mode

This will get user data from the server whenever the user object changes. But looking at the user object, it only makes sense to get the new data when the user.id changes. There could be instances where the user's data changes but the id don't, which won't benefit us in making an API call and getting the data from the server again. Let's say the user's isActive was false initially and later changed to true. In this case, we don't need to get the user's transaction data again because the user is still the same. We can avoid this redundant API call by changing our useEffect dependencies. Here's the refactored component using primitive data types:

function UserInfo({ user }) {
  const { id } = user;

  useEffect(() => {
    getUserData(id);
  }, [id]);

  return (
    ...
  );
}
Enter fullscreen mode Exit fullscreen mode

With the above optimization of useEffect, we will only get the user's transactional data when the user id changes. Remember, in useEffect dependencies, non-primitives (like objects and arrays) are checked for equality only by reference, not by the value. So the following code will always return false:

{ name: "Huzaima" } === { name: "Huzaima" }
Enter fullscreen mode Exit fullscreen mode

2. Avoid needless dependencies

Sometimes, we use useEffect to derive a new state from our current state. Suppose we've a counter which totals the length of the string every time the input value is updated. We can implement that like this:

function CharacterCounter() {
  const [inputValue, setInputValue] = useState("");
  const [compoundStringLength, setCompoundStringLength] = useState(0);

  useEffect(() => {
    if (inputValue) {
      setCompoundStringLength(compoundStringLength + inputValue.length);
    }
  }, [inputValue, compoundStringLength]);

  return (
    ...
  );
}
Enter fullscreen mode Exit fullscreen mode

The above code results in useEffect being executed infinitely. Why? Because of the incorrect dependency. We're updating compoundStringLength inside the useEffect and using the same as a dependency. How do we solve this problem? We need to use compoundStringLength inside the useEffect for computation, so we can't get rid of it. But what we can do is remove it from the dependency array. You might think this will result in an incorrect value of compoundStringLength due to useEffect being a closure. You're right... and wrong. This is correct because useEffect is a closure, so we won't get the correct value of compoundStringLength if we don't use it in the dependency array. Still, there's a way of getting the correct value of compoundStringLength without specifying it in the dependency array. The state setter function (setCompoundStringLength) doesn't only accept value but also a function. The signature of that function looks like this:

setCompoundStringLength: (currentStateValue) => newStateValue;
Enter fullscreen mode Exit fullscreen mode

We can leverage the above function signature in the useEffect to get the correct value of compoundStringLength for our computations without specifying it as a dependency. The refactored optimization of useEffect looks like this:

function CharacterCounter() {
  const [inputValue, setInputValue] = useState("");
  const [compoundStringLength, setCompoundStringLength] = useState(0);

  useEffect(() => {
    if (inputValue) {
      setCompoundStringLength(
        (compoundStringLength) => compoundStringLength + inputValue.length
      );
    }
  }, [inputValue]);

  return (
    ...
  );
}
Enter fullscreen mode Exit fullscreen mode

3. Hoist functions

Any unmemoized function that you define inside the component is recreated on every render. A very naive example is this:

function EmailValidate() {
  const [email, setEmail] = useState("");

  const isEmailValid = (input) => {
    const regex =
      /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

    if (regex.test(input)) return "Email is valid";
    return "Email is invalid";
  };

  return (
    <>
      <input value={email} onChange={(event) => setEmail(event.target.value)} />
      <p>{isEmailValid(email)}</p>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here you can see that the isEmailValid function is a pure function and doesn't depend on the component's props and/or state. We can easily optimize this by extracting the function outside the component. Rewriting the above component will look like this:

const isEmailValid = (input) => {
  const regex =
    /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

  if (regex.test(input)) return "Email is valid";
  return "Email is invalid";
};

function EmailValidate() {
  const [email, setEmail] = useState("");

  return (
    <>
      <input value={email} onChange={(event) => setEmail(event.target.value)} />
      <p>{isEmailValid(email)}</p>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

4. Clean up

Clean up

The primary purpose of useEffect is to handle side effects. Sometimes you have to clean up those side effects like event handler subscriptions or observables. You can return the function having clean-up logic from the function inside useEffect. Let us take an example of an input field which should clear when the user presses the Escape key. We can do so like this:

function Input() {
  const [input, setInput] = useState("");

  const escFunction = useCallback((event) => {
    if (event.key === "Escape") {
      setInput("");
    }
  }, []);

  useEffect(() => {
    document.addEventListener("keydown", escFunction, false);

    return () => {
      document.removeEventListener("keydown", escFunction, false);
    };
  }, [escFunction]);

  return (
    <>
      <input value={input} onChange={(event) => setInput(event.target.value)} />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the above code, we're returning a function from inside the useEffect. That function unsubscribes the keydown event listener, saving us from nasty memory leak problems.

5. Use multiple effects to separate concerns

divide and conquer

Ever heard of divide and conquer? Yup! That's precisely what we need to do here. Aka separation of concerns, a widely practiced principle in software engineering. React team advocates for separation of concerns in useEffect, i.e. using different useEffect for every use case. Suppose we've a component which gets userId and organizationId in props. We make an API call to get user and organization data based on the props. We can write useEffect like this:

useEffect(() => {
  getUser(userId);
  getOrganization(organizationId);
}, [userId, organizationId]);
Enter fullscreen mode Exit fullscreen mode

The above useEffect will work fine. But there's one fundamental problem. If the userId change but the organizationId don't, getOrganization will still get called. Why? Because we've added two separate concerns in a single useEffect. We need to segregate them into two different useEffect. We can rewrite it like this:

useEffect(() => {
  getUser(userId);
}, [userId]);

useEffect(() => {
  getOrganization(organizationId);
}, [organizationId]);
Enter fullscreen mode Exit fullscreen mode

6. Custom hooks

Custom hooks are a great way of extracting side effects, which can be reused by many other components. It's also an elegant way of organizing your code. Suppose we've a custom hook to return details about the input string. We can write that custom hook like this:

function useStringDescription(inputValue) {
  if (!inputValue) {
    return defaultValue;
  } else {
    const wordCount = inputValue.trim().split(/\s+/).length;
    const noOfVowels = inputValue.match(/[aeiou]/gi)?.length || 0;
    return {
      wordCount,
      noOfVowels,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

The useStringDescription hook returns the number of vowels and the word count of a string passed in the argument. We can use this hook like this:

function App() {
  const [inputValue, setInputValue] = useState("");
  const stringDescription = useStringDescription(inputValue);
  const { wordCount, noOfVowels } = stringDescription;

  useEffect(() => {
    console.log("stringDescription changed", stringDescription);
  }, [stringDescription]);

  return (
    ...
  );
}
Enter fullscreen mode Exit fullscreen mode

Let's try to use the above setup and see how many times the useEffect is executed.

unnoptimized rerender

You can see that useEffect is called even when the stringDescription object's value doesn't change. We can destructure the stringDescription object as defined in the 1st rule of this article, or we can optimize our custom hook like this:

function useStringDescription(inputValue) {
  const [stringDescription, setStringDescription] = useState(defaultValue);

  useEffect(() => {
    if (!inputValue) {
      setStringDescription(defaultValue);
    } else {
      const wordCount = inputValue.trim().split(/\s+/).length;
      const noOfVowels = inputValue.match(/[aeiou]/gi)?.length || 0;

      if (
        wordCount !== stringDescription.wordCount ||
        noOfVowels !== stringDescription.noOfVowels
      ) {
        setStringDescription({
          wordCount,
          noOfVowels,
        });
      }
    }
  }, [inputValue, stringDescription.noOfVowels, stringDescription.wordCount]);

  return stringDescription;
}
Enter fullscreen mode Exit fullscreen mode

Now, we're updating the state when any value changes. Let's check out the results:

optimized rerender

See? useEffect didn't get called needlessly because the referential equality holds.

These were the bunch of useEffect optimizations we can use to make the usage of hooks more effective and get rid of nasty and notoriously hard to find bugs caused due to incorrect usage of hooks. Optimizing useEffect in React also gives you a boost in performance. What are your secret useEffect recipes? Let me know on twitter. And don't forget to read my previous blog about promise speed up.

Top comments (0)