Web Development with React
Hands-onBeginner 35 min· React core · 10 of 21

useEffect and fetching data

Learn what an effect is, how the dependency array works, and how to fetch data safely with loading and error states, cleanup and cancellation. You will load the Brew & Bean menu from a JSON file.

In this tutorial you will make the Brew & Bean menu page load its items from a server instead of from a hard-coded array. Along the way you will learn useEffect, the hook for work that must happen outside React's rendering: fetching data, timers, subscriptions and changing the page title.

Prerequisites: a Vite + React project and the lessons *State and events* and *Lists, keys and conditional rendering*. Plain JavaScript (JSX).

Rendering must stay pure

A component function runs every time it renders, and React may run it more than once. So the function body should only calculate JSX from props and state. If you call fetch directly in the body, it fires on every render; and since the result goes into state, which causes another render, you get an endless loop of requests.

An effect is the place for code that reaches outside the component. React renders first, puts the result on the screen, then runs your effect.

src/TitleDemo.jsx
import { useEffect, useState } from 'react';

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

  useEffect(() => {
    document.title = 'Clicked ' + count + ' times';
  }, [count]);

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

export default TitleDemo;

After each render where count changed, React runs the effect and the browser tab title updates.

The dependency array

The second argument tells React when to run the effect again. React compares each value in the array with its value from the previous render.

Three forms of useEffect

CodeRunsTypical use
useEffect(fn)After every renderRare; usually a sign something is wrong
useEffect(fn, [])Once after the first render (twice in development with Strict Mode)Load data once, set up a listener
useEffect(fn, [a, b])After first render and whenever a or b changedRe-fetch when a search term or id changes

The rule: every value from the component (props, state, functions) that the effect uses must be in the array. If you leave one out, the effect keeps using an old copy of it. The ESLint rule react-hooks/exhaustive-deps that comes with the Vite template warns you; do not silence it, fix the cause.

Set up the data

To avoid needing a backend, we use a static JSON file. Anything inside the Vite public folder is served from the site root, so public/menu.json is available at /menu.json. Later you can swap the URL for your real GET /menu API.

public/menu.json
[
  { "id": "espresso", "name": "Espresso", "category": "coffee", "price": 120 },
  { "id": "cappuccino", "name": "Cappuccino", "category": "coffee", "price": 180 },
  { "id": "cold-brew", "name": "Cold Brew", "category": "coffee", "price": 220 },
  { "id": "filter-coffee", "name": "Filter Coffee", "category": "coffee", "price": 80 },
  { "id": "masala-chai", "name": "Masala Chai", "category": "tea", "price": 90 },
  { "id": "croissant", "name": "Butter Croissant", "category": "bakery", "price": 150 },
  { "id": "banana-bread", "name": "Banana Bread", "category": "bakery", "price": 130 },
  { "id": "veg-sandwich", "name": "Veg Grilled Sandwich", "category": "food", "price": 160 }
]

Same menu as before. Open http://localhost:5173/menu.json in the browser while npm run dev is running to confirm it loads.

Fetch with loading and error states

Fetching data takes time and can fail, so a screen has three situations beyond success: loading, error and empty. Beginners often handle only success and show a blank page otherwise. Keep three pieces of state: the data, whether it is loading, and the error.

Build the Menu component

  1. 1Create state: menu (starts as an empty array), loading (starts true) and error (starts null).
  2. 2Write useEffect with an empty dependency array. Inside it, define an async function and call it. The effect function itself must not be async, because it has to return nothing or a cleanup function, not a promise.
  3. 3Call fetch('/menu.json'). fetch does not reject on a 404 or 500, so check response.ok and throw an error yourself.
  4. 4Convert the response with await response.json() and store it with setMenu.
  5. 5Use catch to store the error message and finally to set loading to false, whichever way it went.
  6. 6In the JSX, return early for loading and error, then render the list.
src/Menu.jsx
import { useEffect, useState } from 'react';

function Menu() {
  const [menu, setMenu] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function loadMenu() {
      try {
        const response = await fetch('/menu.json');
        if (!response.ok) {
          throw new Error('Could not load the menu (status ' + response.status + ')');
        }
        const data = await response.json();
        setMenu(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }

    loadMenu();
  }, []);

  if (loading) return <p>Loading menu...</p>;
  if (error) return <p role="alert">Something went wrong: {error}</p>;
  if (menu.length === 0) return <p>The menu is empty today.</p>;

  return (
    <ul>
      {menu.map((item) => (
        <li key={item.id}>
          {item.name} - ₹{item.price}
        </li>
      ))}
    </ul>
  );
}

export default Menu;

Render <Menu /> from App.jsx. The three early returns keep the success JSX simple.

What you should see
For a split second "Loading menu...", then the eight menu items. If you change the URL to '/nothing.json', you see "Something went wrong" (the dev server may return the app's index.html, which fails in response.json(); either way the error message appears).

Test slow and broken networks

In Chrome or Edge DevTools open the Network tab and choose Slow 4G or Offline from the throttling list, then reload. Many Indian users are on patchy mobile data, so your loading and error screens are not optional extras.

Fetching again when something changes

Put the changing value in the dependency array and the effect re-runs. Here is a search box that asks for results as the user types. This brings a new problem: race conditions. If the user types 'ch' then 'chai', two requests are in flight. If the first one finishes last, old results would overwrite the new ones.

The fix is a cleanup function. React runs the function you return from an effect before running the effect again and when the component is removed. We use it to cancel the previous request with an AbortController.

src/SearchMenu.jsx
import { useEffect, useState } from 'react';

function SearchMenu() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

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

    async function search() {
      setLoading(true);
      setError(null);
      try {
        const response = await fetch('/menu.json', { signal: controller.signal });
        if (!response.ok) throw new Error('Status ' + response.status);
        const data = await response.json();
        const q = query.trim().toLowerCase();
        setResults(data.filter((item) => item.name.toLowerCase().includes(q)));
        setLoading(false);
      } catch (err) {
        if (err.name === 'AbortError') return; // a newer request replaced this one
        setError(err.message);
        setLoading(false);
      }
    }

    search();

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

  return (
    <div>
      <input
        value={query}
        onChange={(event) => setQuery(event.target.value)}
        placeholder="Search the menu"
        aria-label="Search the menu"
      />
      {loading && <p>Searching...</p>}
      {error && <p role="alert">{error}</p>}
      <ul>
        {results.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

export default SearchMenu;

A static file ignores the query, so we filter in the browser to keep the example runnable. With a real API you would send the query: fetch('/menu?search=' + encodeURIComponent(query)). The cleanup aborts the previous request each time query changes.

When a request is aborted, fetch rejects with an error named AbortError. That is expected, not a failure, so we ignore it. Without cleanup the code often still works in testing but shows wrong data on slow networks.

Strict Mode runs effects twice

In development, the Vite template wraps your app in <StrictMode>. React deliberately runs each effect, then its cleanup, then the effect again, to reveal missing cleanup. So you will see two requests in the Network tab. This happens only in development; production runs once.

Do not remove StrictMode or add hacks such as a hasFetched flag. Write a correct cleanup and the double run is harmless.

Cleanup for timers and listeners

Cleanup is not only for fetch. Anything you start in an effect, you should stop in its cleanup. A live clock showing the shop's opening status is a classic example.

src/Clock.jsx
import { useEffect, useState } from 'react';

function Clock() {
  const [now, setNow] = useState(new Date());

  useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(id);
  }, []);

  const hour = now.getHours();
  const isOpen = hour >= 8 && hour < 22;

  return (
    <p>
      {now.toLocaleTimeString('en-IN')} - {isOpen ? 'We are open' : 'Closed (8 am to 10 pm)'}
    </p>
  );
}

export default Clock;

Without clearInterval, every time the component was shown and hidden a new timer would be added and none stopped.

You might not need an effect

Effects are for syncing with the outside world. Do not use them for things React can do during render:

Common effect mistakes

MistakeBetter approach
useEffect(() => setFullName(first + ' ' + last), [first, last])const fullName = first + ' ' + last; during render
Filtering a list in an effect and saving it in stateCalculate the filtered list during render
Running code because the user clickedPut it in the onClick or onSubmit handler
Fetching on every render because the dependency array is missingAdd the array with the right values

For real apps, data fetching libraries such as TanStack Query handle caching, retries, loading flags and race conditions for you (see *Managing app state: Context, Zustand and server state*). Knowing how to do it by hand makes those libraries easy to understand and is a common interview question.

Prompt · Ask AI to explain a dependency warning
I am a React beginner. ESLint shows this warning: [paste the exact warning text].

Here is my component:

---
[paste component]
---

Explain in plain English why the warning appears, what bug could happen if I ignore it, and show the smallest correct fix. Do not suggest disabling the rule.

What to expect: A good answer explains stale values, and the fix is usually to add the value to the array or move the function inside the effect.

Infinite loops

An effect that sets state and lists that same state (or a new object or array created in render) in its dependency array can run forever. Objects and arrays are new on every render, so [options] where options = { page: 1 } is declared in the component always looks changed. Use primitive values (page) in the array instead.

Try it: Extend the menu

1. Load the menu from your real FastAPI GET /menu endpoint instead of /menu.json (remember CORS), keeping the loading and error screens. 2. Add a Retry button on the error screen that tries again. Hint: you need a piece of state that changes to re-run the effect. 3. Add a category dropdown and a dependency so changing it re-fetches /menu?category=tea, aborting the previous request. 4. Poll an order's status every 10 seconds with setInterval, and stop when the status is picked_up or the component unmounts.

Key takeaways

  1. Keep rendering pure; use useEffect for fetching, timers, subscriptions and other outside-world work.
  2. The dependency array lists every prop, state value or function the effect uses; [] means once on mount.
  3. Handle loading, error and empty states, and check response.ok because fetch does not reject on 404 or 500.
  4. Return a cleanup function to cancel requests (AbortController), clear timers and remove listeners.
  5. Strict Mode runs effects twice in development; correct cleanup makes that harmless.
  6. Do not use effects for values you can calculate during render or for work that belongs in an event handler.

Check yourself

Finishing the quiz marks the lesson as done.

1. What does an empty dependency array (useEffect(fn, [])) mean?

2. Why should you not write useEffect(async () => { ... })?

3. A user types quickly in a search box and sometimes sees results for an older query. What is the best fix?

4. fetch('/missing') returns a 404 page. What happens to the promise?

5. You see two identical requests in the Network tab in development. What is the most likely reason?

6. You need fullName from first and last state. What is the best approach?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free