본문 바로가기
Dev/React

A Practical Guide to useEffect in React 19

by ArcticBear 2025. 3. 8.
반응형



 1. Introduction

useEffect is one of the most commonly used hooks in React, enabling you to handle side effects—such as data fetching, subscribing to events, or updating the DOM—directly within functional components. While its core usage remains largely the same as previous React versions, React 19 continues to refine how developers approach side effects and concurrency, making it worth revisiting best practices for useEffect.

In this guide, we’ll cover:

  • What useEffect does in React 19
  • Common use cases and dependency management
  • Advanced patterns for performance and concurrency
  • Common pitfalls and how to avoid them

 

 2. The Signature

useEffect(setup, dependencies?)

Where:

  1. setup (Required):
    A function containing the side-effect logic you want to run after React renders (or re-renders) the component.
    • Optionally, this setup function can return a cleanup function. The returned cleanup runs before the next effect is invoked or when the component unmounts.
  2. dependencies? (Optional):
    An array of values that determines when the effect fires.
    • No array: The effect runs after every render.
    • Empty array ([]): The effect runs only once (on mount) and cleans up on unmount.
    • Specified dependencies ([dep1, dep2, ...]): The effect runs whenever any listed dependency changes between renders.

 3. Common Use Cases

 

3.1 Data Fetching

Fetching data from APIs after render is one of the most common patterns:

useEffect(() => {
  fetch("/api/users")
    .then((response) => response.json())
    .then((data) => setUsers(data));
}, []); // Only fetch once on mount

 

3.2 Event Listeners

Attaching (and detaching) window or document event listeners:

useEffect(() => {
  function handleResize() {
    console.log("Window size:", window.innerWidth, window.innerHeight);
  }

  window.addEventListener("resize", handleResize);

  // Cleanup when unmounted
  return () => {
    window.removeEventListener("resize", handleResize);
  };
}, []);
 

3.3 Subscriptions

Subscribing to WebSockets, real-time data streams, or external services:

useEffect(() => {
  const subscription = chatService.subscribe((message) => {
    addMessage(message);
  });

  return () => {
    subscription.unsubscribe();
  };
}, [chatService]);

 

 

 

 4. How Dependencies Work

Dependencies control when your effect re-runs:

  1. No Dependencies
    • The effect runs after every render—useful for logic that needs to check or update on each update cycle.
  2. Empty Array ([])
    • Mimics componentDidMount (and componentWillUnmount for cleanup). The effect fires only once when the component first mounts.
  3. One or More Dependencies
    • The effect re-runs whenever any dependency changes. For example:
useEffect(() => {
  // Update something whenever `count` or `userId` changes
}, [count, userId]);

 

  • Be sure to list all variables your effect uses to avoid stale data or missing updates.

Tip: Enable the react-hooks/exhaustive-deps ESLint rule to catch missing dependencies in your projects.

 

 5. Cleanup Function

A cleanup function can be returned from your setup:

useEffect(() => {
  // Setup
  const timer = setInterval(() => {
    console.log("Interval is running!");
  }, 1000);

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

 

  • React calls the cleanup function when the effect is about to re-run or the component is unmounting.
  • This pattern prevents memory leaks and ensures you don’t leave listeners, timers, or subscriptions active.

 

 6. Example: Fetching Data Once

Below is a more complete example showing how you might fetch data on mount, then store that data in component state:

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

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

  useEffect(() => {
    let isMounted = true;

    fetch("https://jsonplaceholder.typicode.com/users")
      .then((res) => res.json())
      .then((data) => {
        if (isMounted) {
          setUsers(data);
        }
      })
      .catch(console.error);

    // Cleanup to avoid setting state if unmounted
    return () => {
      isMounted = false;
    };
  }, []); // Run once on mount

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

export default UserList;

This approach ensures we only fetch data a single time when the component appears. We also safeguard the state update in case the component unmounts before the fetch resolves.

 

 7. Advanced Patterns for React 19

 

7.1 Concurrency Considerations

With React 19 and concurrent rendering, useEffect can sometimes be delayed or interrupted if higher-priority tasks (like user input) come in. Generally, you don’t need to change your existing approach—React schedules these effects, maintaining a smooth user experience. However, be mindful that effects aren’t run during server-side rendering.

 

7.2 Conditional Effects

If you only want to run an effect when a condition is met:

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

Here, the effect triggers only when shouldFetch transitions to a truthy value.

 

7.3 Performance Optimization

Expensive operations within effects can cause performance bottlenecks. Consider:

  • Memoization (useMemo or caching) if the effect repeatedly does the same work.
  • Debounce or throttle (from a library like Lodash) if your effect is triggered too frequently (e.g., on every keystroke).

 

 8. Common Pitfalls

  1. Infinite Loops
    • Occur when you update state inside the effect without proper dependency control. If the updated state is listed as a dependency, the effect re-runs indefinitely.
  2. Missing Dependencies
    • Failing to list all used variables in dependencies can lead to stale closures or inconsistent data.
  3. Leaking Resources
    • Not cleaning up event listeners, intervals, or subscriptions leads to memory leaks.
  4. Overuse or Misuse
    • If logic can be done outside of rendering cycles or is purely synchronous, you may not need useEffect.

 9. Best Practices

  • Keep Effects Focused: Each effect should handle one concern—like data fetching, logging, or subscriptions.
  • Use Cleanup: Always return cleanup functions for timers or listeners to prevent leaks.
  • Test Carefully: Especially under concurrency, test that your effects behave correctly on various devices and network conditions.
  • Lint Your Hooks: The react-hooks/exhaustive-deps rule helps avoid errors in dependency arrays.
  • Profile for Performance: If you see slow UIs, use the React Profiler to trace frequent re-renders or heavy operations in your effects.

 10. Conclusion

The useEffect hook remains a cornerstone of React 19, enabling concise, powerful side-effect management in functional components. By understanding its signature—

useEffect(setup, dependencies?)

—and following best practices around dependencies, cleanup, and performance, you can build maintainable and efficient applications.

Key Takeaways

  • One Hook, Many Lifecycles: Replace various class lifecycle methods with a single, flexible API.
  • Careful Dependency Control: Correctly handle dependencies to avoid stale data or infinite loops.
  • Cleanup & Performance: Leverage cleanup functions to manage resources and keep your app responsive.
  • Concurrent-Friendly: useEffect seamlessly integrates with React’s concurrency features, helping you deliver a smoother user experience in React 19.

If you have questions, tips, or advanced use cases for useEffect, feel free to share in the comments below. Happy coding!

 

 

반응형