Home » Mastering useEffect in React: Tips and Best Practices

Mastering useEffect in React: Tips and Best Practices

useEffect

Explore the critical React hook, useEffect. Mastering its use can make a vast impact on building high-performing applications, regardless of whether you’re new to React or have been using it for a while.

Managing side effects and component lifecycle events in React is made easier and more declarative with the powerful hook that is useEffect.

With useEffect, you can separate your component’s side effects from the rest of its logic, making your code more organized and easier to maintain.

By carefully defining dependencies, using multiple useEffect calls, and cleaning up resources, you can ensure your useEffect calls are efficient and free of memory leaks.

useEffect is an essential for complex React applications, allowing you to fetch data, handle user events, and manage side effects effectively.

How useEffect Works?

useEffect is a React hook that allows developers to manage side effects in functional components. It takes two arguments: a callback function and an optional dependency array. UseEffect executes the callback function after the component renders. This function can handle side effects such as fetching data or updating the DOM.

The dependency array watches for changes to the variables it contains. If any of those variables change, the callback function is re-executed. If the dependency array is empty, the callback function runs once after the initial render.

By using useEffect, developers can manage side effects in a declarative and controlled way, making it easier to maintain and debug React applications. It’s a powerful and flexible tool for building complex and high-quality applications.


Different types of side effects useEffect can help:

In React, side effects refer to any action that changes the state of the application or has some external effects. Here are some of the examples:

  1. Data fetching: fetching data from a server or external API.
  2. Updating the DOM: updating the content or appearance of the web page.
  3. Timers and intervals: setting up and clearing timers or intervals.
  4. Event listeners: adding or removing event listeners to elements in the DOM.
  5. Subscription management: setting up and managing subscriptions to data or events.
  6. Managing state: updating state and triggering re-renders.
  7. Animations: managing the timing and effects of animations.
  8. Clean-up: cleaning up resources like timers, intervals, or subscriptions.

By using useEffect, developers can manage these side effects in a declarative and controlled way, making it easier to maintain and debug React applications. It’s a powerful tool that can improve the performance, reliability, and scalability of React applications.


Explanation of useEffect dependencies array:

The useEffect dependency array specifies which variables an effect depends on. If any variables in the array change, the effect is re-executed.

1. If the array is empty, the effect is only executed once after the initial render. This is useful for effects that don’t depend on any variables.

useEffect(() => {
	// This effect runs only once after the initial render
	// It does not depend on any variables
	console.log('Effect ran!');
}, []);

2. If the array contains one or more variables, the effect is re-executed when those variables change. This is useful for effects that depend on data or state that can change.

useEffect(() => {
  // This effect runs whenever count changes
  console.log(`Count is ${count}`)
}, [count])

3. If the dependency array is not specified, the effect is re-executed after every render. This can lead to performance issues, so it’s important to only include the necessary variables.

useEffect(() => {
  // This effect runs after every render
  console.log('Effect ran!')
})

By using the dependency array, developers can control when and how often the effect is executed.

This improves the performance and reliability of React applications. It’s a powerful and flexible tool for managing side effects in a declarative and controlled way.


Tips for using useEffect effectively:

  1. Always specify a dependency array to control when the effect runs and prevent unnecessary re-renders.
  2. If the effect depends on multiple variables, use an object or array to combine them in the dependency array.
  3. If you pass an entire object as a dependency in the dependency array of a useEffect hook, it can lead to unnecessary re-renders and decreased performance.
  4. Avoid side effects that cause infinite loops by making sure the effect’s dependencies are updated when necessary.
  5. Use clean-up functions to avoid memory leaks and ensure the effect is properly removed.
  6. Consider splitting effects into separate functions for better readability and maintainability.
  7. Use the useEffect hook only when necessary and consider other hooks like useLayoutEffect or custom hooks for more specific use cases.

Let see how we can effectively use useEffect in different scenarios.

A. Use useEffect to fetch data

import { useState, useEffect } from 'react';

function PostList() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    const fetchPosts = async () => {
      const response = await fetch('https://jsonplaceholder.typicode.com/posts');
      const data = await response.json();
      setPosts(data);
    }
    fetchPosts();
  }, []);

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

export default PostList;

In this example, we’re using useState to manage the state of the component and useEffect to fetch data from an API. The useEffect hook takes a callback function that is executed after the component mounts. In this function, we’re using fetch to get data from the API and setData to update the state of the component with the fetched data.

The empty dependency array in useEffect ensures that the effect only runs once, after the initial render. This prevents the effect from running on every re-render and potentially causing an infinite loop. Once the data is fetched and the state is updated, the component re-renders with the updated data.

B. Avoid infinite loops with useEffect

import { useState, useEffect } from 'react';

function MyComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount((prevCount) => prevCount + 1);
    }, 1000);

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

  return <div>{count}</div>;
}
export default MyComponent;

In this example, we’re using useState to manage the state of the component and useEffect to start a timer that increments the count state every second. We’re also using the cleanup function returned by useEffect to clear the interval when the component unmounts.

The empty dependency array in useEffect ensures that the effect only runs once, after the initial render. If we omit the dependency array, the effect would run on every re-render, causing an infinite loop and eventually crashing the application.

By specifying the dependency array as empty, we’re telling React that the effect doesn’t depend on any variables, so it should only run once. This ensures that the timer is only started once and doesn’t cause an infinite loop.

C. Use multiple useEffect calls

import { useState, useEffect } from 'react';

function PostList() {
	const [posts, setPosts] = useState([]);
	const [error, setError] = useState(null);

	useEffect(() => {
		fetch('https://jsonplaceholder.typicode.com/posts')
			.then((response) => response.json())
			.then((data) => setPosts(data))
			.catch((error) => setError(error));
	}, []);

	useEffect(() => {
		document.title = `My App | ${posts ? posts.title : 'Loading...'}`;
	}, [posts]);

	useEffect(() => {
		const interval = setInterval(() => {
			console.log('Tick');
		}, 1000);

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

	if (error) {
		return <div>Error: {error.message}</div>;
	}

	if (!posts) {
		return <div>Loading...</div>;
	}

	return (
		<>
			<ul>
				{posts.map((post) => (
					<li key={post.id}>
						<h1>{post.title}</h1>
						<p>{post.body}</p>
					</li>
				))}
			</ul>
		</>
	);
}
export default PostList;

In this example, we have three useEffect calls. The first one fetches data from an API and updates the state of the component. The second one updates the title of the document based on the data. The third one sets up an interval to log a message every second.

By using multiple useEffect calls, we can manage different side effects separately and keep the code organized. The dependency arrays ensure that each effect runs at the appropriate time and with the correct data.

D. Clean up with useEffect

useEffect provides a cleanup function that can perform clean-up operations when a component is unmounted or updated.

The cleanup function is returned by the effect callback and is executed when the component is unmounted or re-executed.

Use the cleanup function to remove event listeners, cancel timers, or perform other clean-up operations.

By using the cleanup function, we can ensure that our component is not leaving any “garbage” behind when it is unmounted or updated with new props.

useEffect(() => {
	const handleClick = () => console.log('Clicked!');
	window.addEventListener('click', handleClick);

	return () => {
		console.log('Cleaning up!');
		window.removeEventListener('click', handleClick);
	};
}, []);

In this example, a click event listener is added to the window object in the effect callback. The cleanup function returned by the effect removes the event listener when the component is unmounted.

By using a cleanup function, we can ensure that our component is not leaving any “garbage” behind when it is unmounted or updated with new props.

E. Use useCallback and useMemo with useEffect

useCallback and useMemo can be used to optimize the performance of components by memorizing the functions and values that use useEffect.

By using useCallback and useMemo with useEffect, we can avoid unnecessary re-renders and improve the performance of our components.

Here are some examples of how to use useCallback and useMemo with useEffect:

1. Using useCallback to memoize a function:

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

function MyComponent(props) {
  const { onClick } = props;

  const handleClick = useCallback(() => {
    console.log('Button clicked');
    onClick();
  }, [onClick]);

  useEffect(() => {
    console.log('Component mounted');
    return () => {
      console.log('Component unmounted');
    };
  }, [handleClick]);

  return (
    <button onClick={handleClick}>Click me</button>
  );
}

In this example, we use useCallback to memoize the handleClick function so that it is only created when the onClick prop changes. This ensures that we don’t create a new function on every render.

2. Using useMemo to memoize a value:

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

function MyComponent(props) {
  const { data } = props;

  const expensiveValue = useMemo(() => {
    console.log('Computing expensive value');
    // Compute an expensive value based on the data prop
    return data * 2;
  }, [data]);

  useEffect(() => {
    console.log('Component mounted');
    return () => {
      console.log('Component unmounted');
    };
  }, [expensiveValue]);

  return (
    <div>{expensiveValue}</div>
  );
}

In this example, we use useMemo to memoize the expensiveValue so that it is only recomputed when the data prop changes. This ensures that we don’t compute the expensive value on every render.

Best practices for using useEffect:

  1. Declare all variables used in the useEffect function in the dependency array.
  2. Use multiple useEffect calls instead of a single one with complex logic.
  3. Avoid relying on the component state or props inside useEffect without including them in the dependency array.
  4. Keep the useEffect function small and focused on a specific task.
  5. Use cleanup functions to remove any side effects when the component unmounts.
  6. Use useCallback and useMemo to optimize performance when using functions or computing expensive values in the useEffect.
  7. Avoid creating new functions inside the useEffect that could cause unnecessary re-renders.

Let’s see one code example:

Here’s an example of breaking up complex logic into smaller functions and calling them from useEffect:

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

function MyComponent() {
  const [data, setData] = useState([]);

  useEffect(() => {
    fetchData();
  }, []);

  function fetchData() {
    // fetch data logic
    setData(data);
  }

  return (
    // JSX code
  );
}

export default MyComponent;

In the example above, the fetchData function is called from the useEffect instead of writing everything inside the useEffect function. This makes the code more readable and easier to understand.


How to avoid race conditions in useEffect?

A race condition in React occurs when the output of a function depends on the order of execution of multiple asynchronous actions. This can happen in useEffect when the effect relies on external data or state that may change asynchronously.

There are a few solutions to this problem, and they all involve using the cleanup function provided by the useEffect hook.

A. useEffect cleanup with boolean flag:

To ensure that the component is mounted, we can use a boolean flag. By using the flag, we can update the state only if it’s true, preventing race conditions. When making multiple requests inside a component, we may display the data for the last one only.

   const [data, setData] = useState(null);
	useEffect(() => {
		let isComponentMounted = true;
		const fetchData = async () => {
			const response = await fetch('https://api.example.com/data');
			const data = await response.json();
			if (isComponentMounted) {
				setData(data);
			}
		};
		fetchData();
		return () => {
			isComponentMounted = false;
		};
	}, []);

Each time a component renders, the previous effect is first cleaned up before executing the next effect.

B. useEffect cleanup with AbortController:

We can use the AbortController to cancel previous requests whenever the component is unmounted.

While the previous approach of using boolean flags to handle race conditions in useEffect can work, it is not considered to be the best solution. This is because the requests may continue to be in-flight in the background, even if they are no longer needed.

This could potentially waste the user’s bandwidth and browser resources. Additionally, most browsers limit the maximum number of concurrent requests to a relatively small number, typically 6 to 8, which can cause delays or errors if too many requests are made at once.

   const [posts, setPosts] = useState([]);
	useEffect(() => {
		let abortController = new AbortController();
		const fetchData = async () => {
			try {
				const response = await fetch(
					'https://jsonplaceholder.typicode.com/posts',
					{
						signal: abortController.signal,
					}
				);
				const data = await response.json();
				setPosts(data);
			} catch (error) {
				if (error.name === 'AbortError') {
					// Handling error thrown by aborting request
				}
			}
		};
		fetchData();
		return () => {
			abortController.abort();
		};
	}, []);

The code fetches data from the endpoint using the fetch API and updates the state variable “posts” using the “setPosts” function. It uses an AbortController to cancel previous requests if the component is unmounted.

This helps avoid consuming unnecessary bandwidth and hitting the browser’s limit on concurrent requests.

Developers can also use async/await and the useCallback hook to write more declarative and predictable code that is less prone to race conditions and other issues.


Conclusion:

Mastering useEffect is critical for building high-quality React apps. Dependency array specifies which variables the effect depends on.

Tips: Keep function short, don’t mix state updates and side effects, use the right dependency array, use useCallback and useMemo, use for one-time setup and teardown, and use for declarative effects.

Practice and experiment with useEffect to master it and improve your React applications.


Here are some resources and documentation you may find helpful:

  1. React’s official documentation on the useEffect hook: React’s official documentation
  2. “10 Useful React Hooks for Your Next Project” on Smashing Magazine: 10 Useful React Hooks for you Next Project
  3. “React Hooks Cheat Sheet: Unlock solutions to common problems” on LogRocket: React Hooks Cheat Sheet
  4. “Avoiding Race Conditions and Memory Leaks in React useEffect” on DEV: Avoiding Race Conditions and Memory Leaks

These resources should help you deepen your understanding of useEffect and React in general.


For more on React and complex user interfaces, check out my post on micro-frontend architecture.

I explored breaking down large frontends into smaller, manageable pieces with React and modern tech.

Check it out here: Micro Frontend Architecture Overview

3 thoughts on “Mastering useEffect in React: Tips and Best Practices

Leave a Reply

Your email address will not be published. Required fields are marked *