Optimize Your Effect Hooks

Different things you didn't know about the useEffect hook

·

5 min read

React is a popular JavaScript library for building user interfaces. One of its core features is the ability to manage and update the state of components in response to user interactions and other events. The useEffect hook is a way to perform side effects in functional components. It allows developers to manage changes to the state, as well as synchronize the component with external resources, such as APIs or local storage.

In this article, you will learn how the useEffect works, when to use it, and different approaches to consider when using the hook.

The Effect Hook

The useEffect hook is a combination of two older hooks, componentDidMount and componentDidUpdate. It is called after the component has been rendered to the DOM and it can be used to fetch data, manipulate the DOM, or update the state. It provides a clean way to manage side effects in functional components, without having to use class components or write complex lifecycle methods.

Getting Started

There are two major use cases of the Effect hook:

  • Fetching Data

The useEffect hook can be used to fetch data from APIs and other sources. In this example, we are using the hook to fetch a list of users from an API and updating the state with the results.

import React, { useState, useEffect } from 'react';

const UserList = () => {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users')
      .then(response => response.json())
      .then(data => setUsers(data))
  }, []);

  return { users }
};

export default UserList;

In this example, the useEffect hook is declared with an empty dependency array. This means that the hook will only be called once, when the component is first rendered. This is ideal for fetching data only once from an API (eg.: checking the current logged in user), as we don't want to refetch the data every time the component updates.

  • Keeping a Component Up-to-date

The useEffect hook can also be used to update the state based on changes to other state values. In this example, we are using the hook to update the state with the current time.

import React, { useState, useEffect } from 'react';

const Clock = () => {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    const intervalId = setInterval(() => {
      setTime(new Date());
    }, 1000);

    return () => clearInterval(intervalId);
  }, []);

  return (
    <div>
      <h1>{time.toLocaleTimeString()}</h1>
    </div>
  );
};

export default Clock;

In this example, the useEffect hook is declared with an empty dependency array. This means that the hook will be called once every second, updating the state with the current time. The hook also returns a cleanup function (more on this later) which is used to clear the interval, so that it doesn't continue to run even after the component has been removed from

When Does The Effect Hook Run

The useEffect hook is triggered when any of these conditions are met:

  • When the component first mounts: useEffect is triggerred whenever the page loads for the first time or you refresh the page.

  • Whenever there is a change in the dependency: useEffect is triggerred whenever the value of any of the arguments in the dependency array changes. This ensures that the useEffect is in sync with the arguments in the dependency array.

  • When you declare a useEffect without a dependency array, the component will rerender on every state change.

Optimizing Your useEffect

  • declare the useEffect with an empty dependency array to run the Effect only once (when the component mounts). This can be useful if you want a code block to run only once.

  • add a value to the dependency array to update the Effect whenever the state of that value changes.

  • If the Effect depends on a non-primitive data type (an object or array), memoize the value by wrapping it with useMemo() before adding the memoized value to the dependency array. This prevents the effect from being stuck in an infinite loop of re-renders.

  • If the object is a function, use the useCallback() hook instead of useMemo() for the same reason as above.

  • Use multiple Effects to separate concerns. Each Effect should be used for maintaining a single state, be it fetching data from an API or managing local state.

  • add a cleanup function which will be called before the component unmounts. The cleanup function is triggered whenever the user closes the page or cancels a request.

      import React, { useState, useEffect } from 'react';
    
      const Clock = () => {
        const [time, setTime] = useState(new Date());
    
        useEffect(() => {
          const intervalId = setInterval(() => {
            setTime(new Date());
          }, 1000);
    
          return () => clearInterval(intervalId);
        }, []);
    
        return (
          <div>
            <h1>{time.toLocaleTimeString()}</h1>
          </div>
        );
      };
    
      export default Clock;
    

    In the example above, the returned callback function is used to cleanup the state whenever the Effect unmounts.

    Adding a cleanup function for aborting a fetch:

      import React, { useState, useEffect } from 'react';
    
      const UserList = () => {
        const [users, setUsers] = useState([]);
        const [url, setUrl] = useState('https://jsonplaceholder.typicode.com/users')
    
        useEffect(() => {
          // init an abort controller
          const controller = new AbortController()
    
          fetch(url, { signal: controller.signal})
            .then(response => response.json())
            .then(data => setUsers(data))
    
          // cleanup fn
          return () => controller.abort()
        }, [url]);
    
        return (
          <ul>
            {users.map(user => (
              <li key={user.id}>{user.name}</li>
            ))}
          </ul>
        );
      };
    
      export default UserList;
    

    An AbortController is a JavaScript object used for aborting a fetch request. In the above example, the AbortController is used to abort/cancel the request whenever the user closes the page before the fetch is complete.

    You can also use useState() to create a state for cleaning up your useEffect:

      import React, { useState, useEffect } from 'react';
    
      const UserList = () => {
        const [users, setUsers] = useState([]);
        const [isCancelled, setIsCancelled] = useState(false)
        const [url, setUrl] = useState('https://jsonplaceholder.typicode.com/users')
    
        useEffect(() => {
          if(!isCancelled) {
              fetch(url)
                .then(response => response.json())
                .then(data => setUsers(data))
          }
          // cleanup fn
          return () => setIsCancelled(true)
        }, [url]);
    

    Note: it is recommended to use an abort controller when working with fetch.

Conclusion

Many developers seem to dislike the useEffect hook for several reasons, however, it is a very useful hook and makes state management a whole lot easier if used in the right way. With this article, I hope you've learned some new ways to improve your Effects.

Did you find this article valuable?

Support Paul Saje by becoming a sponsor. Any amount is appreciated!