Ivan Kaminskyi

Jun 17, 202320 min

The Art of Side Effects in React.js: Understanding and Using the useEffect Hook

Getting Started

Introduction to useEffect

Welcome to our journey exploring the useEffect hook in React.js. If you're working with React, you've likely encountered this fundamental hook. Introduced in React 16.8, hooks revolutionized the way we write and manage state and side effects in functional components. Among them, the useEffect hook is specifically designed to handle side effects, such as data fetching, subscriptions, manual DOM manipulations, and many more.

Basic Syntax and Usage

The useEffect hook takes two arguments: a function and an optional dependency array. The function defines the side effect to be run. The dependency array, on the other hand, tells React when to re-run our effect, based on the changes in the values provided.

Here's a simple example of how to use the useEffect hook:

useEffect(() => {
  // Your side effect here
}, [/* dependencies */]);

If you pass an empty array ([]) as the dependency list, the effect will only run once after the first render, mimicking the componentDidMount lifecycle method in class components. If the array contains specific state or prop values, the effect will run whenever any of these values change. If you leave out the array altogether, the effect will run after every render.

Differences Between componentDidMount, componentDidUpdate, and componentWillUnmount

If you're familiar with class components in React, you'll recognize componentDidMount, componentDidUpdate, and componentWillUnmount as key lifecycle methods. How does useEffect compare?

Well, useEffect combines all three lifecycle methods in a more compact and readable manner. You can manage mounting, updating, and cleanup logic all within the same hook. This makes your code cleaner and easier to reason about.

For example, here's how you might use useEffect to replicate these lifecycle methods:

useEffect(() => {
  // This runs after every rendering
  console.log('componentDidMount & componentDidUpdate equivalent');

  // This runs when the component unmounts
  return () => {
    console.log('componentWillUnmount equivalent');
  }
}, [/* dependencies */]);

In the next section, we will delve deeper into the practical uses of useEffect, demonstrating how this powerful hook can simplify your code and make your React applications more efficient and responsive. Stay tuned!


Practical Uses of useEffect

The useEffect hook allows you to perform side effects in function components. It's a close friend to your component, running after every render when specified dependencies change. Let's explore some practical uses.

Fetching Data

One of the most common uses for useEffect is to fetch data from an API. You can use it in combination with the useState hook to fetch and store data:

const [data, setData] = useState(null);

useEffect(() => {
  async function fetchData() {
    const response = await fetch('https://api.example.com');
    const data = await response.json();
    setData(data);
  }

  fetchData();
}, []);  // Empty dependency array means this effect runs once after initial render

Subscribing to Real-time Data

You can also use useEffect to subscribe to real-time data updates, like listening for new messages in a chat app. Ensure to clean up subscriptions when the component is unmounted to avoid memory leaks:

useEffect(() => {
  const unsubscribe = chatAPI.subscribeToNewMessages((newMessage) => {
    // handle new message
  });

  return () => {
    unsubscribe();  // Unsubscribe when the component is unmounted
  };
}, []);

Updating Document Title

With useEffect, you can also interact with the browser API, like updating the document title:

const [title, setTitle] = useState('My Page');

useEffect(() => {
  document.title = title;
}, [title]);  // When the title state changes, the effect runs again

Managing Side Effects

UseEffect allows you to handle side effects that need to occur after render, such as setting up a subscription, timers, or manually adjusting the DOM:

useEffect(() => {
  const timer = setTimeout(() => {
    // action after delay
  }, 1000);

  return () => {
    clearTimeout(timer); // Cleanup
  };
}, []);

Event Listeners

Add event listeners to the document or window objects. Again, don't forget to clean up after yourself to avoid memory leaks:

useEffect(() => {
  function handleResize() {
    // Handle the resize event
  }

  window.addEventListener('resize', handleResize);

  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

Geolocation Updates

You can use useEffect to subscribe to geolocation updates:

useEffect(() => {
  navigator.geolocation.watchPosition(
    (position) => {
      // handle new position
    },
    (error) => {
      // handle error
    }
  );
}, []);

Window Resizing

Here's how you could update state based on window size:

const [windowWidth, setWindowWidth] = useState(window.innerWidth);

useEffect(() => {
  const handleResize = () => {
    setWindowWidth(window.innerWidth);
  };

  window.addEventListener('resize', handleResize);

  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []); // Empty dependency array means this effect runs once

Timing and Intervals

For recurring tasks or actions based on specific time intervals, useEffect can be used to set up timers or intervals:

const [seconds, setSeconds] = useState(0);

useEffect(() => {
  const intervalId = setInterval(() => {
    setSeconds(seconds => seconds + 1);  // update seconds
  }, 1000);

  return () => {
    clearInterval(interval

Id); // cleanup on unmount
  };
}, []);  // Empty dependency array means this effect runs once

These examples cover some of the many ways you can use the useEffect hook to handle side effects in your React application. As you continue your journey with useEffect, you'll find that its use cases are limited only by your imagination!


Common Mistakes

When you start using useEffect, there are some pitfalls you might fall into. Let's cover the most common mistakes developers make with useEffect and how to avoid them.

Misunderstanding Dependencies

Dependencies help useEffect know when to run again. However, misunderstandings about their role can lead to unintentional behavior. useEffect only re-runs when its dependencies change.

// Incorrect
useEffect(() => {
  setCount(count + 1);
}, [count]);  // This will cause an infinite loop because count changes on every render

// Correct
useEffect(() => {
  setCount(count + 1);
}, []);  // This will run only once after the first render

Missing Dependencies

The linter rule react-hooks/exhaustive-deps warns us when we might have missed dependencies or added unnecessary ones. You should generally heed its advice. Ignoring it might lead to bugs.

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

function increment() {
  setCount(count + 1);
}

useEffect(() => {
  window.addEventListener('keydown', increment);
  // Cleanup
  return () => {
    window.removeEventListener('keydown', increment);
  };
}, []);  // This will not warn, but the event listener uses the initial version of `increment`

Infinite Loops

Infinite loops occur when the function called inside useEffect causes an update to the dependencies, causing useEffect to run again.

const [value, setValue] = useState(0);

useEffect(() => {
  setValue(value + 1);  // Updating value causes the effect to run again
}, [value]);  // Causes an infinite loop

Overusing useEffect

Sometimes, we might be tempted to put all state updates in useEffect, but that's not its purpose. If a state update can be done during rendering, it's usually better to do so.

// Incorrect
const [count, setCount] = useState(0);

useEffect(() => {
  setCount(1);
}, []);

// Correct
const [count, setCount] = useState(1);

Improper Cleanup

Failing to clean up after effects may cause memory leaks. Any function returned from the effect function will be run when the component unmounts, and before the effect runs again.

// Incorrect
useEffect(() => {
  const intervalId = setInterval(() => {
    // Do something
  }, 1000);
}, []);  // No cleanup function

// Correct
useEffect(() => {
  const intervalId = setInterval(() => {
    // Do something
  }, 1000);

  return () => {
    clearInterval(intervalId);  // Cleanup function
  };
}, []);

Forgetting to Unsubscribe Event Listeners

Failing to unsubscribe from event listeners can also cause memory leaks. Always remember to remove any listeners you add.

// Incorrect
useEffect(() => {
  window.addEventListener('resize', handleResize);
}, []);  // No cleanup function

// Correct
useEffect(() => {
  window.addEventListener('resize', handleResize);

  return () => {
    window.removeEventListener('resize', handleResize);  // Cleanup function
  };
}, []);

Memory Leaks

Memory leaks occur when you keep using resources after you no longer need them. Always clean up resources like timers, intervals, and listeners when you're done with them.

// Incorrect
useEffect(() => {
  const intervalId = setInterval(() => {
    // Do something

  }, 1000);

  // No cleanup function
}, []);

// Correct
useEffect(() => {
  const intervalId = setInterval(() => {
    // Do something
  }, 1000);

  return () => {
    clearInterval(intervalId);  // Cleanup function
  };
}, []);

Avoiding these common mistakes will make your use of useEffect more effective and your React application more robust. Stay tuned for advanced uses of useEffect in the next section!


Advanced Uses of useEffect

While useEffect is excellent for running side effects and syncing state, it also has some advanced uses. Let's explore them:

Debouncing and Throttling API Requests

By combining useEffect with debouncing or throttling, you can limit the number of API requests made as the user types into a search box, improving performance:

const [query, setQuery] = useState('');
const [results, setResults] = useState([]);

const debouncedQuery = useDebounce(query, 500); // Custom debounce hook

useEffect(() => {
  async function fetchData() {
    // Fetch data with debounced query
    const response = await fetch(`https://api.example.com?q=${debouncedQuery}`);
    const data = await response.json();
    setResults(data);
  }

  fetchData();
}, [debouncedQuery]); 

Conditionally Running Effects

Though it's usually best to have useEffect run on every render, in some cases, you may want to conditionally run an effect. In those cases, you can set up a condition inside your useEffect:

useEffect(() => {
  if (loggedIn) {
    // Perform effect only if the user is logged in
  }
}, [loggedIn]); 

Context Changes

You can use useEffect to perform actions based on changes in the React context:

const theme = useContext(ThemeContext);

useEffect(() => {
  // Apply theme to body
  document.body.style.backgroundColor = theme.background;
}, [theme]); 

Concurrent Mode and Suspense

useEffect also works with Concurrent Mode and Suspense, allowing you to create more fluid and responsive user interfaces:

const resource = createResource(fetchData());  // Create a resource using a custom function

useEffect(() => {
  // Access resource in an effect to start loading it
  resource.read();
}, [resource]);

Animations and Transitions

Use useEffect to manage animations and transitions. By adjusting CSS properties, you can create visually appealing effects:

useEffect(() => {
  const element = document.getElementById("myElement");
  element.style.transition = "all 0.5s ease-in-out";
  element.style.transform = "translateX(100px)";
}, []);

WebSocket Communication

useEffect can be used to establish WebSocket connections and listen for messages:

const [messages, setMessages] = useState([]);

useEffect(() => {
  const socket = new WebSocket('ws://example.com');

  socket.onmessage = (event) => {
    setMessages(prevMessages => [...prevMessages, event.data]);
  };

  return () => {
    socket.close();
  };
}, []);

Custom Hooks with useEffect

useEffect can be used inside custom hooks to abstract and reuse stateful logic:

function useDocumentTitle(title) {
  useEffect(() => {
    document.title = title;
  }, [title]);
}

// Usage
useDocumentTitle('My Page');

These advanced uses illustrate the flexibility and power of useEffect. In the next section, we'll delve into some real-world case studies of useEffect usage.


Case Studies

Understanding the theory is great, but let's dive into some practical, real-world examples where useEffect shines.

Data Fetching and Rendering Optimization

Let's say you're building a movie listing app that fetches data from a remote API. With useEffect, you can easily fetch the data and render it when available:

const [movies, setMovies] = useState([]);

useEffect(() => {
  async function fetchData() {
    const response = await fetch('https://api.example.com/movies');
    const data = await response.json();
    setMovies(data);
  }

  fetchData();
}, []);

This code makes a single request to fetch the movies data when the component mounts, then updates the movies state, triggering a re-render with the new data.

Real-time Notifications using WebSockets

In a chat application, you might need to subscribe to a WebSocket for real-time notifications:

const [messages, setMessages] = useState([]);

useEffect(() => {
  const socket = new WebSocket('ws://chat.example.com');

  socket.onmessage = (event) => {
    setMessages(prevMessages => [...prevMessages, event.data]);
  };

  return () => {
    socket.close();
  };
}, []);

Here, useEffect sets up a WebSocket connection and listens for incoming messages. When a message arrives, it adds the message to the state, triggering a re-render.

Dark Mode Implementation

useEffect can be used to implement a dark mode in your application. It can observe changes in the theme state and apply the appropriate CSS classes to the body element:

const [theme, setTheme] = useState('light');

useEffect(() => {
  document.body.className = theme;
}, [theme]);

Infinite Scrolling

Implementing infinite scrolling with useEffect involves listening for the scroll event and checking if the user has scrolled to the bottom of the page:

const [items, setItems] = useState([]);

useEffect(() => {
  window.addEventListener('scroll', handleScroll);

  return () => {
    window.removeEventListener('scroll', handleScroll);
  };
}, []);

function handleScroll() {
  if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
    // User has scrolled to the bottom, load more items
    loadMoreItems();
  }
}

function loadMoreItems() {
  // Fetch and append more items to the list
}

Image Preloading

In an image gallery app, you might want to preload images for a smoother user experience. useEffect can help:

const [images, setImages] = useState([]);

useEffect(() => {
  images.forEach((image) => {
    const img = new Image();
    img.src = image;
  });
}, [images]);

This code creates a new Image object for each image in the state and sets its src property, triggering the browser to load the image.

These case studies demonstrate useEffect in action in realistic scenarios, showing how this versatile hook can help solve common challenges in React applications.


Testing useEffect hook

Introduction to Testing Hooks

Before delving into specific strategies, let's talk about the philosophy of testing hooks, specifically useEffect.

Testing hooks can be a bit different from testing regular JavaScript functions due to their "hidden" nature. React hooks, like useEffect, useState, useContext, etc., are not directly exposed for testing. Therefore, the idea is to test the behavior or effects caused by these hooks rather than the hooks themselves.

In other words, when testing components that use hooks, focus on the output and behavior of the component rather than the internal implementation of the hooks. Essentially, we adopt a black-box testing methodology where we only care about the input and the output, not how the output is produced.

For testing React components and hooks, tools like Jest and React Testing Library are often used. Jest is a JavaScript testing framework that provides functionalities for test suites, test cases, and assertions. React Testing Library is a lightweight solution for testing React components. It provides utility functions on top of react-dom and react-dom/test-utils and encourages a mental model of testing from the user's perspective, not implementation details.

Remember, when writing tests, we want them to be:

  • Clear: Tests should be easy to read and understand.
  • Reliable: Tests should consistently return the same result given the same conditions.
  • Comprehensive: Tests should cover as many cases as possible, including edge cases.
  • Fast: Tests should run quickly to not slow down development.

In the following sections, we'll explore specific strategies and examples for testing useEffect with Jest and React Testing Library.

Testing Strategies for useEffect

Testing is a critical part of any application. Here we will discuss a few strategies to test components that use the useEffect hook. We will use the react-testing-library and Jest for our examples.

Testing API Calls

Consider a component that fetches data from an API using useEffect. We can mock the API call and assert that the data is rendered correctly:

import { render, waitFor } from '@testing-library/react';
import axios from 'axios';
import Component from './Component';

jest.mock('axios');

test('fetches and displays data', async () => {
  axios.get.mockResolvedValue({ data: 'Hello, World!' });

  const { getByTestId } = render(<Component />);

  await waitFor(() => expect(getByTestId('data')).toHaveTextContent('Hello, World!'));
});

Testing Event Listeners

Testing a component that sets up an event listener in useEffect involves firing the event and checking its effect:

import { render, fireEvent } from '@testing-library/react';
import Component from './Component';

test('increments count when window is clicked', () => {
  const { getByTestId } = render(<Component />);

  fireEvent.click(window);

  expect(getByTestId('count')).toHaveTextContent('1');
});

Testing Cleanup

If your useEffect performs cleanup, you'll want to ensure the cleanup code runs as expected. React Testing Library will automatically call cleanup functions when a component is unmounted:

import { render, fireEvent } from '@testing-library/react';
import Component from './Component';

jest.spyOn(window, 'removeEventListener');

test('removes event listener on unmount', () => {
  const { unmount } = render(<Component />);

  unmount();

  expect(window.removeEventListener).toHaveBeenCalled();
});

Testing Delayed Effects

For effects that run after a delay (like with setTimeout), jest's fake timers can be helpful:

import { render } from '@testing-library/react';
import jest from 'jest';
import Component from './Component';

jest.useFakeTimers();

test('changes text after delay', () => {
  const { getByTestId } = render(<Component />);

  jest.advanceTimersByTime(1000);

  expect(getByTestId('text')).toHaveTextContent('Changed');
});

Remember, when testing components with useEffect, focus on the end result of the effect, not the implementation details. Think about what the user should see or be able to do when the effect is working correctly.

With these testing strategies, you should be well-equipped to write robust tests for your components that use the useEffect hook!

Tools for Testing useEffect

When it comes to testing useEffect and other React hooks, there are several tools available to help you write effective and robust tests. Below are some of the key ones:

Jest

Jest is a popular JavaScript testing framework developed by Facebook. It's packed with features like a zero-configuration setup for JavaScript and TypeScript, snapshot testing, async function testing, and mock functions. One of the most relevant features for testing useEffect is Jest's fake timers, which allow you to fast-forward or slow down time, a feature particularly useful for testing effects that involve delays or intervals.

React Testing Library

React Testing Library is a set of helper functions for testing React components. It's designed to encourage writing tests that closely resemble how your React components are used. With React Testing Library, you can render components, fire events, find elements, and assert on their output. When testing useEffect, React Testing Library is useful because it automatically cleans up after each test, ensuring that effects don't leak between tests.

Mock Service Worker (MSW)

When testing components that make network requests inside useEffect, it can be helpful to control the responses to those requests. Mock Service Worker is a tool for mocking network requests directly in the browser. With MSW, you can create a mock server that intercepts your app's network requests and returns controlled responses.

React Hooks Testing Library

While generally it's recommended to test the component using the hooks rather than the hooks themselves, sometimes you might find it necessary to test custom hooks directly. For such cases, there's React Hooks Testing Library. This library allows you to create a simple test harness for React hooks that handles running them within the body of a function component, as well as updating and unmounting them.

JSDom

JSDom is a JavaScript implementation of the DOM and browser APIs that runs in Node.js. Jest uses JSDom to create a mock DOM for your tests, allowing you to test components and hooks that interact with the DOM.

Remember, the choice of tools often depends on your specific needs and the nature of your project. However, these tools, when used properly, can significantly streamline your testing process and help you build more reliable applications. In the next sections, we'll take a deeper look at how to use these tools in various testing scenarios.

Examples of Test Cases for useEffect

After discussing the available tools, let's delve into some concrete examples of how to write test cases for components that use the useEffect hook.

Testing Network Requests

Suppose you have a component that fetches a list of users from an API when it mounts. With useEffect, you might implement it like this:

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

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

  useEffect(() => {
    axios.get('https://api.example.com/users')
      .then(response => setUsers(response.data));
  }, []);

  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
};

export default UserList;

You can test this component using Jest and React Testing Library:

import { render, waitFor } from '@testing-library/react';
import axios from 'axios';
import UserList from './UserList';

jest.mock('axios');

test('fetches and displays users', async () => {
  const users = [{ id: 1, name: 'John Doe' }, { id: 2, name: 'Jane Doe' }];

  axios.get.mockResolvedValue({ data: users });

  const { findAllByRole } = render(<UserList />);

  const items = await findAllByRole('listitem');

  expect(items).toHaveLength(2);
});

Testing Event Listeners

Here is a simple component that listens for a click event on the window and updates its state when the window is clicked:

import { useEffect, useState } from 'react';

const ClickTracker = () => {
  const [clicks, setClicks] = useState(0);

  useEffect(() => {
    const incrementClicks = () => setClicks(clicks => clicks + 1);

    window.addEventListener('click', incrementClicks);

    return () => {
      window.removeEventListener('click', incrementClicks);
    };
  }, []);

  return <p>Window clicks: {clicks}</p>;
};

export default ClickTracker;

Here's a test case for this component using Jest and React Testing Library:

import { render, fireEvent } from '@testing-library/react';
import ClickTracker from './ClickTracker';

test('increments count when window is clicked', () => {
  const { getByText } = render(<ClickTracker />);

  fireEvent.click(window);

  expect(getByText('Window clicks: 1')).toBeInTheDocument();
});

Testing Conditional Effects

Let's say we have a component that conditionally runs an effect based on a prop. We can write tests to ensure the effect runs under the right conditions:

import { useEffect } from 'react';

const Notification = ({ message, user }) => {
  useEffect(() => {
    if (user.loggedIn) {
      // Show notification
    }
  }, [message, user]);

  // Render notification
};

export default Notification;

Here's a test case for this component using Jest and React Testing Library:

import { render } from '@testing-library/react';
import Notification from './Notification';

jest.spyOn(window, 'Notification').mockImplementation(() => {});

test('shows notification when user is logged in', () => {
  render(<Notification message="Hello, World!" user={{ loggedIn: true }} />);

  expect(window.Notification).toHaveBeenCalledWith('Hello, World!');
});

These examples should give you a solid starting point for writing your test cases when working

with useEffect. The key is always to think about what the user sees and does and to write your tests accordingly.


Conclusion

Throughout this comprehensive guide, we've explored the useEffect hook in React.js from various angles, covering practical applications, common mistakes, advanced usage, real-life case studies, and effective testing strategies.

By understanding and applying the useEffect hook properly, you can manage side effects in your functional components with ease, improving the maintainability, readability, and overall quality of your codebase. The examples and strategies we've discussed provide valuable insights and tools for leveraging useEffect in a range of scenarios, from simple data fetching to real-time data subscriptions, window resizing, and complex interactions with APIs and event listeners.

However, it's equally important to be aware of the potential pitfalls and common mistakes associated with useEffect. Whether it's infinite loops, improper cleanup, missing dependencies, or overuse of the hook, recognizing and avoiding these issues can help you write more robust and efficient code.

Furthermore, the advanced use cases presented here extend the reach of useEffect, enabling you to handle debouncing and throttling, conditional effects, and interactions with contexts, animations, WebSockets, and more. By leveraging custom hooks, you can encapsulate and reuse your effect logic, enhancing the modularity of your code.

We've also explored a variety of real-world case studies, demonstrating how useEffect can be employed in the implementation of features like dark mode, infinite scrolling, and real-time notifications. By examining these examples, you'll be able to draw inspiration for your projects and see how useEffect can be put to work in different contexts.

Finally, we delved into the realm of testing, explaining why and how to test components that use useEffect. Using tools like Jest and React Testing Library, you can create a robust testing suite that ensures your components behave as expected under a variety of conditions.

Overall, the useEffect hook is a powerful tool in the React developer's toolkit. By mastering its usage and understanding its nuances, you can build more dynamic, interactive, and responsive applications. Happy coding!

Tags:
React
Share:

Related Posts

Get The Latest Insights straight To Your Inbox For Free!

Subscribe To The NewsletterSubscribe To The NewsletterSubscribe To The Newsletter