Skip to content

Expert · React concepts · topic 60 of 64 · 40 min

Effects, data fetching and hooks

Explanation

useEffect runs code after React updates the screen, to keep a component in sync with something outside React: a timer, a browser API or data from a server. The dependency array says when it runs again, and the cleanup function stops old work. Many things do not need an effect at all. useRef keeps a value without re-rendering, and custom hooks let components share logic.

What is an effect?

Rendering should be pure: work out the JSX from props and state, nothing else. But some code must talk to the world outside React: set a timer, change document.title, connect to a chat server, or fetch data. useEffect runs such code after React has updated the screen. Its job is to keep the component in sync with an outside system.

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

function CartTitle({ count }) {
  useEffect(() => {
    document.title = `Cart (${count})`;
  }, [count]);

  return <h1>Your cart</h1>;
}
The effect runs after rendering, and again whenever count changes (React code, runs in a React project)

The dependency array

You writeThe effect runs
useEffect(fn, [])After the first render only (when the component appears)
useEffect(fn, [city])After the first render, and again after any render where city changed
useEffect(fn)After every render (rarely what you want)

List every value from the component that the effect reads: props, state and functions declared inside the component. React compares each one with Object.is. React's official lint plugin, eslint-plugin-react-hooks, warns you when you forget one. Do not silence it; a missing dependency means the effect uses old values.

Cleanup: return a function that stops the work

javascript
function OtpTimer() {
  const [seconds, setSeconds] = useState(30);

  useEffect(() => {
    const id = setInterval(() => {
      setSeconds((s) => (s > 0 ? s - 1 : 0));
    }, 1000);
    return () => clearInterval(id); // cleanup
  }, []);

  return <p>Resend OTP in {seconds}s</p>;
}
React runs the returned cleanup before the effect runs again and when the component is removed. Use it to clear timers, remove listeners and close connections (React code, runs in a React project)
Tip: In development, StrictMode runs every effect twice when a component first appears: setup, cleanup, setup. This is on purpose, to show you missing cleanups. It does not happen in the production build.

Fetching data: loading, error and the ignore flag

javascript
function TrainStatus({ trainNo }) {
  const [status, setStatus] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let ignore = false;
    setLoading(true);
    setError(null);

    async function load() {
      try {
        const res = await fetch(`/api/trains/${trainNo}`);
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const data = await res.json();
        if (!ignore) setStatus(data);
      } catch (err) {
        if (!ignore) setError(err.message);
      } finally {
        if (!ignore) setLoading(false);
      }
    }
    load();

    return () => {
      ignore = true; // a newer trainNo replaced this request
    };
  }, [trainNo]);

  if (loading) return <p>Loading...</p>;
  if (error) return <p role="alert">Could not load the status: {error}</p>;
  return <p>{status.name} is running {status.delayMinutes} minutes late</p>;
}
Three pieces of state for the three situations: loading, error and data (React code, runs in a React project)
Think of it like this: You order a chai, then change your order to a coffee. If the chai still arrives later, you say: "ignore that one, I changed my order". The ignore flag does the same. When trainNo changes, the cleanup marks the old request as ignored, so a slow old answer can never overwrite the new one.
Careful: Do not make the effect function itself async (useEffect(async () => ...)). An async function returns a promise, but React expects nothing or a cleanup function. Declare an async function inside the effect and call it, as above.

You might not need an effect

  • Data you can calculate from props or state: calculate it while rendering. No effect, no extra state.
  • Something that happens because the user did something (clicked Buy, submitted a form): do it in the event handler, not in an effect.
  • Resetting all the state of a component when a prop changes: give the component a key, such as <Profile key={userId} />.
  • Loading data in a big app: a framework's data loading or a library usually does it better than a hand-written effect.
javascript
// Avoid: extra state and an effect
const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);

// Better: calculate during render
const fullName = firstName + ' ' + lastName;
The effect version renders twice and can show an old name for a moment (React code, runs in a React project)

useRef and custom hooks

javascript
import { useRef } from 'react';

function SearchBox() {
  const inputRef = useRef(null);
  return (
    <>
      <input ref={inputRef} aria-label="Search" />
      <button onClick={() => inputRef.current.focus()}>Search</button>
    </>
  );
}
useRef gives a box { current } that stays the same between renders; changing it does not re-render. Here it holds the input element (React code, runs in a React project)
javascript
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const saved = localStorage.getItem(key);
      return saved !== null ? JSON.parse(saved) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch {
      // storage is full or blocked: keep working without saving
    }
  }, [key, value]);

  return [value, setValue];
}

// In any component:
const [city, setCity] = useLocalStorage('city', 'Pune');
A custom hook is a function whose name starts with use and which calls other hooks. It shares logic, not state: each component gets its own copy (React code, runs in a React project)
Tip: In real projects, many teams load server data with a library such as TanStack Query, or with the data loading built into a framework such as Next.js or React Router. These handle caching, retries and race conditions for you. Also, the React Compiler is an optional build tool from the React team that can memoise (remember) components and values for you, so you need useMemo and useCallback by hand much less often. Learn the basics in this module first.

Watch the lesson

Effects, data fetching and hooks · 1/7

render = pure effect = sync with outside

Rendering should only calculate JSX. Talking to the outside world happens in an effect.

Prefer a video by a teacher? Find free videos on “react useEffect data fetching cleanup useRef custom hooks tutorial” on YouTube ↗

In real life: A live train status page

On a train running status page, you type a train number. An effect with [trainNo] as its dependency fetches the status, shows "Loading..." and then the delay, or a clear error if the network fails. The ignore flag makes sure a slow answer for the old number never replaces the new one. A second effect refreshes the status every 60 seconds with setInterval and clears it in the cleanup, and a useLocalStorage hook remembers your last train number.

Practice (0/3 done)

Task 1 · Code · Javascript

Write the loading logic an effect would call: async function loadTrainStatus(fetchFn, trainNo). Call fetchFn with the URL "/api/trains/<trainNo>" (for example "/api/trains/12951"). If the response is ok, return { status: "success", data } where data is await response.json(). If response.ok is false, return { status: "error", error: "HTTP <status>" } (for example "HTTP 404"). If fetchFn throws (no network), return { status: "error", error: <the error message> }. The tests pass a fake fetch.

Task 2 · Code · Javascript

Fix a race condition. weatherEffect(fetchFn, city, setData) is the body of an effect: it starts loading the weather for city and calls setData with the answer. It must return a cleanup function. Right now, if the user switches from Delhi to Mumbai and the Delhi answer arrives last, Delhi's weather overwrites Mumbai's. Add an ignore flag: set it to true in the cleanup, and only call setData when ignore is still false.

Task 3 · Practice

In your Vite project, build a "Country info" page that fetches from a free public API you choose (or a local JSON file in the public folder). A select box chooses the item; an effect with that value in its dependency array loads the data with loading and error states and an ignore flag. Add a custom hook useLocalStorage to remember the last choice, and a button that focuses the select with useRef. Watch the effect run twice in development (StrictMode) in the console.

Quick quiz

Question 1

1.When does useEffect(fn, []) run?

Question 2

2.What is the cleanup function of an effect for?

Question 3

3.What problem does the ignore flag in a fetching effect solve?

Question 4

4.fullName is firstName + ' ' + lastName. What is the best way to get it in a component?

Question 5

5.What happens when you change ref.current?

Read more (free)

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

← Previous: Lists, keys and forms