Introduction to useEffect

Introduction

useEffect is one of the most important hooks in React. It allows developers to perform side effects in functional components. Side effects are operations that can affect something outside the component itself, such as fetching data from an API, manipulating the DOM, setting up subscriptions, or handling timers.

Before hooks, React developers relied on class component lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount to manage side effects. useEffect unifies these behaviors in a single, versatile hook that works in functional components.

In this article, we will explore:

  1. What useEffect is.
  2. How to run effects on mount, update, and unmount.
  3. How cleanup functions work.
  4. How dependency arrays affect execution.
  5. Best practices for using useEffect effectively.

What is useEffect?

useEffect is a hook that allows functional components to handle side effects. It runs after the component has rendered, providing a way to interact with external systems or execute operations that depend on the DOM or state.

Syntax

useEffect(() => {
  // effect logic
}, [dependencies]);
  • The first argument is a function that contains the effect logic.
  • The second argument is an optional dependency array that determines when the effect runs.

Basic Example of useEffect

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

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

  useEffect(() => {
console.log("Component rendered or count changed");
}); return (
<div>
  <p>Count: {count}</p>
  <button onClick={() => setCount(count + 1)}>Increment</button>
</div>
); }

Explanation

  • The effect runs after every render, including the first render.
  • Every time the state changes, the component re-renders and triggers the effect.

Running Effects Only on Mount

Sometimes, you want to run an effect only once, similar to componentDidMount in class components. You can achieve this by passing an empty dependency array.

Example

useEffect(() => {
  console.log("Component mounted");

  // Fetch data or initialize something here
}, []);
  • The empty array [] ensures that the effect runs only once, on initial mount.
  • Subsequent re-renders do not trigger this effect.

Running Effects on State or Prop Changes

To run an effect whenever a particular state or prop changes, include it in the dependency array.

Example

function Example({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
fetch(https://jsonplaceholder.typicode.com/users/${userId})
  .then((res) => res.json())
  .then((data) => setUser(data));
}, [userId]); // effect runs whenever userId changes return <div>{user ? user.name : "Loading..."}</div>; }

Explanation

  • userId is included in the dependency array.
  • The effect runs whenever userId changes.
  • This is useful for fetching new data when props or state change.

Cleanup Functions

Effects may need cleanup to prevent memory leaks or unintended behavior. For example, when using timers, subscriptions, or event listeners.

Example: Timer Cleanup

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

  useEffect(() => {
const interval = setInterval(() =&gt; {
  setCount((prev) =&gt; prev + 1);
}, 1000);
// Cleanup function
return () =&gt; clearInterval(interval);
}, []); // Run only once on mount return <p>Count: {count}</p>; }

Explanation

  • The cleanup function runs before the component unmounts or before the effect runs again.
  • This prevents memory leaks and ensures timers, subscriptions, or listeners are properly removed.

Running Effects on Update Only

Sometimes, you want an effect to skip the initial mount and run only on updates. You can do this by tracking a flag or using useRef.

Example

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

function UpdateOnlyExample() {
  const [count, setCount] = useState(0);
  const isInitialMount = useRef(true);

  useEffect(() => {
if (isInitialMount.current) {
  isInitialMount.current = false;
} else {
  console.log("Count updated:", count);
}
}, [count]); return (
&lt;div&gt;
  &lt;p&gt;Count: {count}&lt;/p&gt;
  &lt;button onClick={() =&gt; setCount(count + 1)}&gt;Increment&lt;/button&gt;
&lt;/div&gt;
); }

Explanation

  • useRef is used to track whether the component is mounting.
  • The effect only runs on updates, not on the first render.

Dependency Arrays Explained

The dependency array controls when an effect is executed:

  1. Empty array []: Run once on mount.
  2. No array: Run after every render.
  3. Array with values [dep1, dep2]: Run whenever any dependency changes.

Example: Multiple Dependencies

function Example({ a, b }) {
  useEffect(() => {
console.log("Effect runs when a or b changes");
}, [a, b]); }
  • The effect runs only when either a or b changes.
  • Avoid including unnecessary dependencies to prevent excessive re-renders.

Common Pitfalls with useEffect

1. Forgetting Dependencies

useEffect(() => {
  console.log(count);
}, []); // count is missing, effect may be stale
  • Missing dependencies can lead to stale values.
  • ESLint’s react-hooks/exhaustive-deps can help catch these issues.

2. Infinite Loops

useEffect(() => {
  setCount(count + 1); // Causes infinite re-renders
}, [count]);
  • Avoid updating state directly in the effect without proper checks.

3. Running Effects Too Frequently

  • Including too many dependencies can trigger the effect unnecessarily.
  • Optimize by minimizing dependencies and using useCallback or useMemo when passing functions or objects.

Real-World Examples of useEffect

1. Fetching Data from API

function Users({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
fetch(https://jsonplaceholder.typicode.com/users/${userId})
  .then(res =&gt; res.json())
  .then(data =&gt; setUser(data));
}, [userId]); return <div>{user ? user.name : "Loading..."}</div>; }

2. Event Listener

function WindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
const handleResize = () =&gt; setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () =&gt; window.removeEventListener("resize", handleResize);
}, []); return <p>Window width: {width}</p>; }

3. Subscriptions

function Chat({ roomId }) {
  useEffect(() => {
const unsubscribe = subscribeToRoom(roomId, message =&gt; {
  console.log(message);
});
return () =&gt; unsubscribe();
}, [roomId]); }

Combining Multiple Effects

It’s often better to separate effects instead of combining everything in one. This makes code easier to read and maintain.

Example

function Example() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  // Effect for count
  useEffect(() => {
console.log("Count updated:", count);
}, [count]); // Effect for text useEffect(() => {
console.log("Text changed:", text);
}, [text]); }

Best Practices for useEffect

  1. Keep effects focused on a single responsibility.
  2. Use dependency arrays correctly to avoid stale closures.
  3. Include cleanup functions for subscriptions, timers, or listeners.
  4. Split multiple unrelated effects into separate useEffect hooks.
  5. Avoid using effects to derive state; compute derived values inside render.
  6. Use ESLint rules for hooks to catch dependency issues.

Advanced Patterns

1. Fetching Data with AbortController

function User({ userId }) {
  useEffect(() => {
const controller = new AbortController();
fetch(https://jsonplaceholder.typicode.com/users/${userId}, {
  signal: controller.signal
})
  .then(res =&gt; res.json())
  .then(data =&gt; console.log(data))
  .catch(err =&gt; {
    if (err.name === "AbortError") console.log("Fetch aborted");
  });
return () =&gt; controller.abort();
}, [userId]); }

2. Conditional Effects

useEffect(() => {
  if (!userId) return;
  console.log("Fetching user data for", userId);
}, [userId]);
  • Run effects only when certain conditions are met.

Comments

Leave a Reply

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