React Effect Hooks

React components are mainly responsible for describing what the user interface should look like for a particular state and set of props. However, real applications often need to do things that are not simply about rendering UI.

For example, an application may need to:

  • Fetch data from an API
  • Start or stop a timer
  • Subscribe to browser events
  • Listen for changes from an external service
  • Update the browser document title
  • Connect to a WebSocket
  • Read information from a browser API
  • Clean up resources when a component is no longer needed

These operations are called side effects.

React provides the useEffect Hook for synchronizing a component with something outside of React.

Understanding Effects is important because useEffect is powerful, but it is also one of the Hooks that beginners commonly misuse. Many pieces of code that appear to need an Effect can actually be handled directly during rendering or inside an event handler.

In this chapter, you will learn what Effects are, how useEffect works, how dependency arrays control when Effects run, how cleanup functions work, how to fetch data and use timers, how to work with subscriptions and browser APIs, and how to avoid common useEffect mistakes.

What Is an Effect in React?

An Effect is code that synchronizes a React component with something outside the component’s normal rendering process.

Examples include:

  • Connecting to a server
  • Fetching information from an external API
  • Setting up a browser event listener
  • Starting a timer
  • Updating an external library
  • Subscribing to a data source
  • Changing a browser API value

The important idea is that Effects are about synchronization with external systems.

Consider this component:

function Welcome() {
  return <h1>Welcome to Web4Learners</h1>;
}

This component only describes the UI. It does not need an Effect.

Now imagine that the component needs to listen for the browser’s online/offline status:

useEffect(() => {
  window.addEventListener("online", handleOnline);
  window.addEventListener("offline", handleOffline);

  return () => {
    window.removeEventListener("online", handleOnline);
    window.removeEventListener("offline", handleOffline);
  };
}, []);

The browser is an external system from React’s point of view.

The Effect connects the component to that external system and the cleanup function disconnects it.

What Does useEffect Do?

useEffect is a React Hook that allows a component to perform an Effect after React has rendered the component.

You import it from React:

import { useEffect } from "react";

A basic Effect looks like this:

useEffect(() => {
  console.log("Effect ran");
});

The function passed to useEffect is called the setup function.

React runs this setup function after the component has been committed to the DOM.

A more realistic example is:

import { useEffect } from "react";

function PageTitle() {
  useEffect(() => {
    document.title = "Web4Learners";
  });

  return <h1>Welcome to Web4Learners</h1>;
}

Here, React renders the heading, and the Effect updates the browser tab title.

Why Does React Need Effects?

React rendering should ideally remain predictable.

Given the same props and state, a component should describe the same UI.

For example:

function UserProfile({ name }) {
  return <h1>Hello, {name}</h1>;
}

Rendering this component does not need to communicate with anything outside React.

But consider:

document.title = "User Profile";

This changes something outside React’s rendered UI.

Or:

window.addEventListener("resize", handleResize);

This creates a relationship with the browser.

Or:

fetch("/api/users");

This communicates with an external server.

These are examples of operations that may need an Effect.

The Basic useEffect Syntax

The general syntax is:

useEffect(() => {
  // Effect code

  return () => {
    // Cleanup code
  };
}, [dependencies]);

There are three important parts:

Setup Function

The first argument is the setup function:

useEffect(() => {
  console.log("Effect started");
});

Cleanup Function

The setup function can optionally return another function:

useEffect(() => {
  console.log("Effect started");

  return () => {
    console.log("Effect cleaned up");
  };
});

The returned function is called the cleanup function.

Dependency Array

The second argument is the dependency array:

useEffect(() => {
  console.log("Effect ran");
}, [userId]);

The dependency array tells React which reactive values the Effect depends on.

When Does an Effect Run?

The behavior of an Effect depends largely on whether you provide a dependency array.

There are three common patterns.

Effect Without a Dependency Array

useEffect(() => {
  console.log("Effect ran");
});

Without a dependency array, the Effect runs after every completed render.

For example:

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

  useEffect(() => {
    console.log("Rendered");
  });

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

Every state update causes a render, so the Effect runs after each render.

This can be useful in specific situations, but it is often unnecessary.

Effect With an Empty Dependency Array

useEffect(() => {
  console.log("Effect ran");
}, []);

The empty dependency array means the Effect does not depend on any reactive values from the component.

It generally runs after the initial mount.

For example:

useEffect(() => {
  document.title = "Web4Learners";
}, []);

This is commonly used when setting up something that only needs to be initialized for the component’s lifetime.

However, an empty dependency array should not be added simply to “make an Effect run once.” The Effect should accurately represent what it depends on.

Effect With Dependencies

useEffect(() => {
  console.log("User changed:", userId);
}, [userId]);

React runs the Effect after the initial render and again when userId changes.

For example:

function UserProfile({ userId }) {
  useEffect(() => {
    console.log("Load user:", userId);
  }, [userId]);

  return <h1>User Profile</h1>;
}

If the parent passes a different userId, the Effect synchronizes with the new user.

Understanding the Dependency Array

The dependency array is one of the most important parts of useEffect.

Consider:

useEffect(() => {
  console.log(userId);
}, [userId]);

The Effect uses userId, so userId belongs in the dependency array.

Another example:

useEffect(() => {
  console.log(userId);
  console.log(theme);
}, [userId, theme]);

The Effect depends on both values.

React compares dependency values between renders. When one of them changes, React runs the cleanup from the previous Effect, if there is one, and then runs the new setup.

Why Dependencies Matter

Suppose you write:

useEffect(() => {
  console.log(userId);
}, []);

If userId can change during the component’s lifetime, this Effect does not correctly describe that dependency.

The Effect may continue using the value from the render where it was created.

Instead, use:

useEffect(() => {
  console.log(userId);
}, [userId]);

This tells React:

Synchronize this Effect whenever the user ID changes.

Effects and Props

Effects can depend on props.

function UserProfile({ userId }) {
  useEffect(() => {
    console.log("Loading user:", userId);
  }, [userId]);

  return <h1>User Profile</h1>;
}

If the parent changes userId, React renders the component with the new value and runs the Effect again.

Effects and State

Effects can also depend on state.

function SearchPage() {
  const [query, setQuery] = useState("");

  useEffect(() => {
    console.log("Search query changed:", query);
  }, [query]);

  return (
    <input
      value={query}
      onChange={(event) => setQuery(event.target.value)}
    />
  );
}

Every time query changes, the Effect synchronizes with the new value.

Cleanup Functions

Some Effects create resources or relationships that need to be removed.

Examples include:

  • Event listeners
  • Timers
  • Subscriptions
  • WebSocket connections
  • External library connections

For these situations, an Effect can return a cleanup function.

useEffect(() => {
  // Setup

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

The cleanup function should undo or disconnect what the setup function created.

Why Cleanup Is Important

Consider an event listener:

useEffect(() => {
  window.addEventListener("resize", handleResize);
}, []);

If the component is removed, the event listener can remain attached unless it is removed.

A better implementation is:

useEffect(() => {
  window.addEventListener("resize", handleResize);

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

The Effect establishes the connection, and the cleanup removes it.

This pattern is extremely important:

Setup what you need. Clean up what you created.

When Does Cleanup Run?

Cleanup runs before the Effect is re-run because its dependencies changed.

For example:

useEffect(() => {
  console.log("Connect:", roomId);

  return () => {
    console.log("Disconnect:", roomId);
  };
}, [roomId]);

If roomId changes:

Previous Effect cleanup
        ↓
New Effect setup

When the component is removed from the screen, the final cleanup also runs.

This makes Effects useful for synchronizing external resources with the component’s current state.

A Subscription Example

Imagine a component subscribing to a chat room:

useEffect(() => {
  const connection = createConnection(roomId);

  connection.connect();

  return () => {
    connection.disconnect();
  };
}, [roomId]);

When roomId changes, React disconnects from the old room and connects to the new room.

This is much safer than continuously creating new connections without removing the old ones.

React Strict Mode and Cleanup

When developing with React Strict Mode, you may sometimes see an Effect setup and cleanup happen more than once during development.

For example:

Setup
Cleanup
Setup

This development behavior helps reveal Effects that do not correctly clean up after themselves.

It does not mean your production application will necessarily perform the same sequence.

If an Effect breaks when setup and cleanup happen in sequence, that usually indicates that the Effect needs better synchronization or cleanup logic.

Fetching Data with useEffect

Fetching data is a common reason developers use useEffect.

For example:

import { useEffect, useState } from "react";

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

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

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

The Effect starts the request after the component renders.

When the request completes, the data is stored in state, causing another render.

Handling Loading State

A real application should usually represent the loading state.

function Users() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch("/api/users")
      .then((response) => response.json())
      .then((data) => {
        setUsers(data);
      })
      .finally(() => {
        setLoading(false);
      });
  }, []);

  if (loading) {
    return <p>Loading users...</p>;
  }

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

Now the UI has a clear loading state.

Handling Fetch Errors

Network requests can fail, so applications should also handle errors.

function Users() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function loadUsers() {
      try {
        const response = await fetch("/api/users");

        if (!response.ok) {
          throw new Error("Failed to load users");
        }

        const data = await response.json();
        setUsers(data);
      } catch (error) {
        setError(error.message);
      } finally {
        setLoading(false);
      }
    }

    loadUsers();
  }, []);

  if (loading) {
    return <p>Loading...</p>;
  }

  if (error) {
    return <p>Error: {error}</p>;
  }

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

This gives the user three possible states:

Loading
   ↓
Success

or

Loading
   ↓
Error

For production applications, data-fetching libraries or framework-provided data-loading mechanisms may be preferable to manually managing every request with useEffect.

Fetching Data When a Dependency Changes

Suppose a page displays information for a particular user.

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

  useEffect(() => {
    async function loadUser() {
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();

      setUser(data);
    }

    loadUser();
  }, [userId]);

  if (!user) {
    return <p>Loading...</p>;
  }

  return <h1>{user.name}</h1>;
}

The dependency array contains:

[userId]

Therefore, when userId changes, the Effect runs again.

Avoiding Race Conditions in Data Fetching

When requests can overlap, an older request might finish after a newer request.

For example:

Request A → user 1
Request B → user 2

If Request B finishes first and Request A finishes later, Request A could incorrectly overwrite the state.

An AbortController can help cancel an outdated request:

useEffect(() => {
  const controller = new AbortController();

  async function loadUser() {
    try {
      const response = await fetch(`/api/users/${userId}`, {
        signal: controller.signal,
      });

      const data = await response.json();
      setUser(data);
    } catch (error) {
      if (error.name !== "AbortError") {
        setError(error.message);
      }
    }
  }

  loadUser();

  return () => {
    controller.abort();
  };
}, [userId]);

When userId changes, the previous request can be aborted.

Using Timers with useEffect

Timers are another common Effect use case.

For example, you can create an interval:

useEffect(() => {
  const intervalId = setInterval(() => {
    console.log("Running...");
  }, 1000);

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

The cleanup function is important because the interval should stop when the component is no longer using it.

Creating a Countdown Timer

function Countdown() {
  const [seconds, setSeconds] = useState(10);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setSeconds((current) => {
        if (current <= 1) {
          clearInterval(intervalId);
          return 0;
        }

        return current - 1;
      });
    }, 1000);

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

  return <p>{seconds} seconds remaining</p>;
}

Notice the functional state update:

setSeconds((current) => current - 1);

This is useful because the next state depends on the previous state.

Using setTimeout

You can also use setTimeout.

useEffect(() => {
  const timeoutId = setTimeout(() => {
    console.log("Message displayed");
  }, 2000);

  return () => {
    clearTimeout(timeoutId);
  };
}, []);

If the component disappears before the timeout executes, the cleanup prevents the pending timeout from continuing.

Subscriptions

A subscription means that your component starts listening to an external source.

Examples include:

  • WebSocket messages
  • Chat updates
  • Online status
  • External stores
  • Browser events
  • Real-time notifications

A basic pattern looks like this:

useEffect(() => {
  const unsubscribe = subscribeToUpdates((data) => {
    console.log(data);
  });

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

The subscription is created during setup.

The returned function removes the subscription.

WebSocket Example

A WebSocket connection is another example:

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

  socket.addEventListener("message", handleMessage);

  return () => {
    socket.removeEventListener("message", handleMessage);
    socket.close();
  };
}, []);

The cleanup closes the connection when it is no longer needed.

If the connection depends on a value such as a room ID:

useEffect(() => {
  const socket = createRoomConnection(roomId);

  socket.connect();

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

Now the connection follows the current roomId.

Browser APIs

The browser provides many APIs that React does not control directly.

Examples include:

  • window
  • document
  • localStorage
  • sessionStorage
  • navigator
  • IntersectionObserver
  • ResizeObserver
  • Geolocation
  • matchMedia

Some of these APIs can be used directly in event handlers or during rendering when appropriate. Others require synchronization and are good candidates for Effects.

Browser Window Events

Suppose you want to monitor the browser width.

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

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth);
    }

    window.addEventListener("resize", handleResize);

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

  return <p>Window width: {width}px</p>;
}

The Effect establishes the relationship with the browser’s resize event.

The cleanup removes it.

In environments that render React on the server, accessing browser-only APIs such as window during initial rendering requires additional care because those APIs do not exist on the server.

Updating the Document Title

Another simple browser API example is document.title.

function ProductPage({ product }) {
  useEffect(() => {
    document.title = product.name;
  }, [product.name]);

  return <h1>{product.name}</h1>;
}

When the product name changes, the browser tab title is updated.

Using Local Storage

You can synchronize state with localStorage.

function ThemePreference() {
  const [theme, setTheme] = useState("light");

  useEffect(() => {
    localStorage.setItem("theme", theme);
  }, [theme]);

  return (
    <button onClick={() => setTheme("dark")}>
      Use Dark Theme
    </button>
  );
}

Whenever theme changes, the Effect updates localStorage.

For reading initial values, however, you may not need an Effect. You can initialize state from storage when the environment supports it.

Avoiding Unnecessary Effects

One of the most important lessons about useEffect is:

Not every piece of code that runs after rendering needs an Effect.

Developers sometimes use useEffect for calculations that React can perform directly during rendering.

For example, this is unnecessary:

function ProductList({ products }) {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(products.length);
  }, [products]);

  return <p>Total products: {count}</p>;
}

The number of products can be calculated directly:

function ProductList({ products }) {
  const count = products.length;

  return <p>Total products: {count}</p>;
}

There is no need to store derived information as separate state.

Avoiding Duplicate State

Consider:

const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [fullName, setFullName] = useState("");

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

fullName is derived from firstName and lastName.

Instead:

const fullName = `${firstName} ${lastName}`;

This is simpler and avoids an unnecessary render cycle.

Calculate During Rendering When Possible

If a value can be calculated from existing props and state, calculate it during rendering.

For example:

function Cart({ items }) {
  const total = items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );

  return <p>Total: ₹{total}</p>;
}

There is no reason to use:

useEffect(() => {
  setTotal(...);
}, [items]);

The total is derived data.

Event Handlers vs Effects

A useful question is:

Is this operation caused by rendering, or by a specific user interaction?

If something happens because the user clicked a button, it often belongs in the event handler.

For example:

function SaveButton({ user }) {
  function handleSave() {
    saveUser(user);
  }

  return <button onClick={handleSave}>Save</button>;
}

There is no need to write:

useEffect(() => {
  saveUser(user);
}, [user]);

That would cause saving to be tied to rendering rather than the user’s explicit action.

Effects Should Synchronize, Not Orchestrate Everything

A common mistake is using Effects to coordinate application logic.

For example:

useEffect(() => {
  if (isLoggedIn) {
    loadDashboard();
  }
}, [isLoggedIn]);

Sometimes this is appropriate if the Effect is genuinely synchronizing with an external system.

But if loadDashboard() is directly caused by a button click, form submission, or navigation action, that operation may belong in the event handler or a dedicated data-loading mechanism instead.

Common useEffect Mistakes

useEffect is useful, but several mistakes appear frequently in React applications.

Mistake: Using Effects for Derived Values

Avoid:

const [fullName, setFullName] = useState("");

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

Prefer:

const fullName = `${firstName} ${lastName}`;

Mistake: Missing Dependencies

Avoid:

useEffect(() => {
  fetch(`/api/users/${userId}`);
}, []);

If the Effect needs to respond to changes in userId, include it:

useEffect(() => {
  fetch(`/api/users/${userId}`);
}, [userId]);

Mistake: Incorrect Cleanup

Avoid creating a listener without removing it:

useEffect(() => {
  window.addEventListener("resize", handleResize);
}, []);

Prefer:

useEffect(() => {
  window.addEventListener("resize", handleResize);

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

Mistake: Creating Timers Without Clearing Them

Avoid:

useEffect(() => {
  setInterval(() => {
    console.log("Running");
  }, 1000);
}, []);

Prefer:

useEffect(() => {
  const id = setInterval(() => {
    console.log("Running");
  }, 1000);

  return () => {
    clearInterval(id);
  };
}, []);

Mistake: Updating State in an Effect Without a Good Reason

Consider:

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

This creates a loop because changing count causes another render, which runs the Effect again.

A component can quickly get stuck in repeated updates.

Effects that update state should have a clear reason and should not accidentally create render loops.

Mistake: Suppressing Dependency Warnings

If your Effect uses a reactive value, simply removing it from the dependency array is usually not the right solution.

For example:

useEffect(() => {
  console.log(userId);
}, []);

Do not silence the warning without understanding why the dependency is intentionally excluded.

Instead, restructure the code or include the dependency when the Effect genuinely depends on it.

Mistake: Putting Event Logic in an Effect

Suppose a user clicks a button to purchase something.

Do not use an Effect merely to detect a state change:

useEffect(() => {
  if (purchased) {
    sendPurchaseRequest();
  }
}, [purchased]);

If the purchase request is directly caused by the button click, it is usually clearer to perform the action in the event handler:

function handlePurchase() {
  sendPurchaseRequest();
}

The Effect should be reserved for synchronization that follows from rendering or changing reactive values.

Mistake: Forgetting That Objects and Functions Have Identity

Consider:

const options = {
  roomId,
  serverUrl,
};

useEffect(() => {
  connect(options);
}, [options]);

Because a new object is created during each render, options can have a new identity each time.

This can cause the Effect to run more often than expected.

One solution is to create the object inside the Effect:

useEffect(() => {
  const options = {
    roomId,
    serverUrl,
  };

  connect(options);
}, [roomId, serverUrl]);

Similarly, functions created during rendering can have new identities.

Before adding useCallback simply to stabilize a function, first consider whether the Effect can be structured more simply.

A Practical Way to Think About Dependencies

When writing an Effect, ask:

What external system am I synchronizing with?

Then ask:

Which reactive values determine that synchronization?

For example:

useEffect(() => {
  const connection = connectToRoom(roomId);

  return () => {
    connection.disconnect();
  };
}, [roomId]);

The external system is the room connection.

The synchronization depends on:

roomId

Therefore:

[roomId]

is the dependency list.

A Complete Effect Example

Here is a more realistic example that combines fetching, loading, error handling, dependencies, and cleanup.

import { useEffect, useState } from "react";

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    async function loadUser() {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(`/api/users/${userId}`, {
          signal: controller.signal,
        });

        if (!response.ok) {
          throw new Error("Unable to load user");
        }

        const data = await response.json();
        setUser(data);
      } catch (error) {
        if (error.name !== "AbortError") {
          setError(error.message);
        }
      } finally {
        if (!controller.signal.aborted) {
          setLoading(false);
        }
      }
    }

    loadUser();

    return () => {
      controller.abort();
    };
  }, [userId]);

  if (loading) {
    return <p>Loading user...</p>;
  }

  if (error) {
    return <p>Error: {error}</p>;
  }

  return <h1>{user?.name}</h1>;
}

This example demonstrates several important concepts:

  • useEffect
  • Dependency arrays
  • Fetching data
  • Loading state
  • Error state
  • Cleanup
  • AbortController
  • Synchronizing with changing userId

A Simple Mental Model for Effects

You can think of an Effect as a synchronization process:

React renders component
        ↓
Effect setup runs
        ↓
External system is synchronized
        ↓
Dependencies change
        ↓
Old Effect cleanup
        ↓
New Effect setup

When the component is removed:

Component removed
        ↓
Final cleanup

This mental model is more useful than thinking of useEffect as simply “run this code after render.”

Effect Lifecycle Example

Consider:

useEffect(() => {
  console.log("Connected");

  return () => {
    console.log("Disconnected");
  };
}, [roomId]);

The lifecycle can look like:

Initial render
     ↓
Connected

roomId changes
     ↓
Disconnected
     ↓
Connected

Component unmounts
     ↓
Disconnected

This pattern is particularly useful for connections and subscriptions.

When Should You Use useEffect?

Use useEffect when your component needs to synchronize with an external system.

Common examples include:

SituationEffect?
Fetching external dataOften
WebSocket connectionYes
Browser event listenerYes
TimerYes
External subscriptionYes
Updating document.titleYes
Calculating total from propsNo
Formatting a stringNo
Filtering an array for displayUsually no
Handling button clicksUsually no
Form submissionUsually no
Deriving state from existing stateUsually no

The key is not to memorize the table. Instead, ask whether you are synchronizing with something outside React.

Effects in a Web4Learners Tutorial Page

Imagine Web4Learners has a tutorial page with a selected lesson.

function TutorialPage({ lessonId }) {
  const [lesson, setLesson] = useState(null);

  useEffect(() => {
    async function loadLesson() {
      const response = await fetch(`/api/lessons/${lessonId}`);
      const data = await response.json();

      setLesson(data);
    }

    loadLesson();
  }, [lessonId]);

  return (
    <main>
      {lesson ? (
        <>
          <h1>{lesson.title}</h1>
          <p>{lesson.description}</p>
        </>
      ) : (
        <p>Loading lesson...</p>
      )}
    </main>
  );
}

Here, the Effect is appropriate because the component is synchronizing its UI with external lesson data.

When the user selects another lesson, lessonId changes and the Effect fetches the corresponding lesson.

Effects and Performance

Effects can indirectly affect application performance if they trigger unnecessary work.

For example:

useEffect(() => {
  setFilteredProducts(
    products.filter((product) => product.category === category)
  );
}, [products, category]);

If filteredProducts is only derived data for rendering, this introduces unnecessary state and another render.

Instead:

const filteredProducts = products.filter(
  (product) => product.category === category
);

If the calculation is genuinely expensive, React’s performance tools and memoization techniques can be considered separately. Do not automatically add useMemo or `useCallback just because an Effect runs.

Effects Are Not a Replacement for Event Handlers

This distinction is important.

An event handler answers:

What should happen because the user did something?

An Effect answers:

What should happen to synchronize this component with an external system?

For example:

function Newsletter() {
  function handleSubscribe() {
    subscribeUser();
  }

  return (
    <button onClick={handleSubscribe}>
      Subscribe
    </button>
  );
}

The subscription action is directly caused by the user’s click, so the event handler is the natural place for it.

An Effect would be appropriate if the component needed to synchronize an external subscription with a changing prop or state.

Effects and State Updates

Effects often update state after synchronizing with an external system.

For example:

useEffect(() => {
  const handleOnline = () => {
    setOnline(true);
  };

  const handleOffline = () => {
    setOnline(false);
  };

  window.addEventListener("online", handleOnline);
  window.addEventListener("offline", handleOffline);

  return () => {
    window.removeEventListener("online", handleOnline);
    window.removeEventListener("offline", handleOffline);
  };
}, []);

The browser event is external.

The React state represents that external information inside the component.

That is a good reason to use an Effect.

Best Practices for useEffect

Keep Effects focused on one synchronization concern whenever possible.

Identify the External System

Before writing an Effect, ask:

What am I connecting to or synchronizing with?

If the answer is “nothing,” you may not need an Effect.

Keep Dependencies Accurate

Do not intentionally omit dependencies simply to reduce Effect executions.

Instead, restructure the Effect when necessary.

Always Clean Up What You Create

If you create:

setInterval(...)

clean it up with:

clearInterval(...)

If you add:

addEventListener(...)

remove it with:

removeEventListener(...)

If you create a subscription, unsubscribe from it.

If you create a connection, disconnect it.

Avoid Duplicate State

If a value can be calculated from existing props or state, prefer deriving it during rendering.

Keep Event Logic in Event Handlers

Actions caused directly by user interaction usually belong in event handlers.

Handle Async Operations Carefully

When using Effects for requests, consider:

  • Loading states
  • Error states
  • Cancellation
  • Changing dependencies
  • Race conditions
  • Stale results

Test Effects With Strict Mode

If your Effect produces unexpected behavior during development, check whether setup and cleanup are correctly symmetrical.

Effect Hooks Cheat Sheet

ConceptPurpose
useEffectSynchronize with external systems
Setup functionStarts the synchronization
Cleanup functionStops or reverses the synchronization
Dependency arrayControls when synchronization is re-established
[]Effect has no reactive dependencies
[value]Re-synchronize when value changes
No dependency arrayRun after every completed render
FetchingCan synchronize component with external data
TimerStart timer and clean it up
SubscriptionSubscribe and unsubscribe
Browser APISynchronize with browser-managed systems
Derived valueUsually calculate during rendering instead
User actionUsually handle with event handlers

Practice Exercise

Create a small React application called Web4Learners Online Status.

The application should:

  • Display whether the browser is online
  • Listen for online and offline events
  • Store the current status in state
  • Update the UI when the browser connection changes
  • Remove the event listeners during cleanup

A possible starting point is:

import { useEffect, useState } from "react";

function OnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }

    function handleOffline() {
      setIsOnline(false);
    }

    window.addEventListener("online", handleOnline);
    window.addEventListener("offline", handleOffline);

    return () => {
      window.removeEventListener("online", handleOnline);
      window.removeEventListener("offline", handleOffline);
    };
  }, []);

  return (
    <p>
      Status: {isOnline ? "Online" : "Offline"}
    </p>
  );
}

export default OnlineStatus;

This exercise demonstrates the complete Effect pattern:

External browser events
        ↓
useEffect
        ↓
Event listeners
        ↓
React state
        ↓
UI update
        ↓
Cleanup

Key Takeaways

  • An Effect allows a React component to synchronize with an external system.
  • useEffect is the Hook used to create Effects.
  • Effects run after React commits the component’s rendered output.
  • The dependency array describes the reactive values that the Effect depends on.
  • An empty dependency array means the Effect has no reactive dependencies.
  • Without a dependency array, an Effect runs after every completed render.
  • Cleanup functions undo or disconnect resources created by an Effect.
  • Timers should be cleared during cleanup.
  • Event listeners should be removed during cleanup.
  • Subscriptions should be unsubscribed.
  • Connections should be disconnected.
  • Data fetching can be performed with Effects, but larger applications may benefit from dedicated data-loading solutions.
  • Effects can depend on props and state.
  • Do not use Effects simply to calculate derived values.
  • Avoid storing duplicate or derived state when it can be calculated during rendering.
  • User interactions generally belong in event handlers.
  • Missing dependencies can cause Effects to use outdated values.
  • Incorrect dependencies can cause unnecessary Effect executions.
  • Effects that update state can accidentally create infinite render loops.
  • Objects and functions created during rendering can affect dependency identity.
  • A good Effect has a clear synchronization purpose.
  • Setup and cleanup should work together as a balanced pair.

The most important idea to remember is simple:

Use Effects to synchronize React with the world outside React, not as a general-purpose place to run code after rendering.

Leave a Comment