> ## Content Index
> Fetch the complete content index at: https://reactpractice.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Data fetching with useEffect - why you should go straight to react-query, even for simple apps
- URL: https://reactpractice.dev/articles/data-fetching-with-useeffect-why-you-should-go-straight-to-react-query-even-for-simple-apps/
- Published: 2025-06-11T10:21:43.000Z
- Updated: 2026-06-01T01:57:37.000Z
- Author: Corina Udrescu
- Tags: Data fetching, #corina-2, Articles, #Import 2026-06-09 12:07

Data fetching with `useEffect` is **easy to use** and **great for quick one-off or practice apps**.  
I've used it myself for a long time - but recently, I've come to the conclusion it's **best to just use react-query**, even for the simplest cases.  
Why? Because to **use it well** it takes so much **boilerplate**, it's not really worth.  
Here's what I mean.

### A basic example

Let's take a super simple example - fetching a list of pokemons from the [Poke API](https://pokeapi.co/?ref=reactpractice.dev).  
The user can input how many pokemon to load and the value is passed as the `limit` when fetching the data:

![](https://storage.ghost.io/c/7c/fc/7cfc444f-a1c9-4dd3-a87e-8dcc2f1180be/content/images/2025/06/Pasted-image-20250611105110.png)

The basic code for this using `useEffect` is as follows:

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

export default function App() {
  const [pokemon, setPokemon] = useState([]);
  const [limit, setLimit] = useState(2);
  useEffect(() => {
    async function fetchPokemon() {
      const response = await fetch(
        `https://pokeapi.co/api/v2/pokemon?limit=${limit}`
      );
      const data = await response.json();
      setPokemon(data.results);
    }

    fetchPokemon();
  }, [limit]);

  return (
    <div className="App">
      <h1>Hello Poke API!</h1>
      <div>
        <p>Please enter how many pokemon to load</p>
        <input
          type="number"
          name="limit"
          value={limit}
          onChange={(e) => setLimit(e.target.value)}
        />
      </div>
      <ul>
        {pokemon.map((p) => (
          <li key={p.name}>{p.name}</li>
        ))}
      </ul>
    </div>
  );
}

```

It's quite straightforward, right? But it's actually buggy!  
The docs point this out - "[avoid: fetching without cleanup logic](https://react.dev/learn/you-might-not-need-an-effect?ref=reactpractice.dev#fetching-data)":

![](https://storage.ghost.io/c/7c/fc/7cfc444f-a1c9-4dd3-a87e-8dcc2f1180be/content/images/2025/06/Pasted-image-20250611103841.png)

## The problem with the basic case

With effects, you still need to respect the rule of **not breaking the app if you run the effect twice** (which is checked by default in development mode with StrictMode).

So, even if it's ok to use `useEffect` for data fetching, you should make sure that the request that "[is not relevant anymore does not keep affecting your application](https://react.dev/learn/synchronizing-with-effects?ref=reactpractice.dev#fetching-data)". You can do this with an **ignore** flag:

![](https://storage.ghost.io/c/7c/fc/7cfc444f-a1c9-4dd3-a87e-8dcc2f1180be/content/images/2025/06/Pasted-image-20250611104236.png)

  
Why is this a problem? Because if you don't ignore previous requests, you have no control over which requests finishes first - and your risk having the app show the results of the first request instead of the second, even if the state changed! This is called [a race condition and the official docs have a great example over here](https://react.dev/learn/you-might-not-need-an-effect?ref=reactpractice.dev#fetching-data).

Here's how our updated example would look like:

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

export default function App() {
  const [pokemon, setPokemon] = useState([]);
  const [limit, setLimit] = useState(2);
  useEffect(() => {
    let ignore = false;

    async function fetchPokemon() {
      const response = await fetch(
        `https://pokeapi.co/api/v2/pokemon?limit=${limit}`
      );
      const data = await response.json();
      if (!ignore) {
        setPokemon(data.results);
      }
    }

    fetchPokemon();

    return () => {
      ignore = true;
    };
  }, [limit]);

  return // same as before ....

```

This works, but you can see how the code is getting a bit **harder to read**.

## Handling loading and error states

With the basic flow in place, we can now add logic to handle **loading and error states**.  
For testing this, I recommend simulating a slower Internet connection, so you can actually see the loading indicator! You can do this by setting the [Chrome DevTools "Throttling" setting](https://www.debugbear.com/blog/chrome-devtools-network-throttling?ref=reactpractice.dev) to "3G".

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

export default function App() {
  const [pokemon, setPokemon] = useState([]);
  const [limit, setLimit] = useState(2);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    let ignore = false;

    async function fetchPokemon() {
      setIsLoading(true);
      try {
        const response = await fetch(
          `https://pokeapi.co/api/v2/pokemon?limit=${limit}`
        );
        const data = await response.json();
        if (!ignore) {
          setPokemon(data.results);
          setIsLoading(false);
        }
      } catch (e) {
        if (!ignore) {
          setError(e.message);
          setPokemon([]);
          setIsLoading(false);
        }
      }
    }

    fetchPokemon();

    return () => {
      ignore = true;
    };
  }, [limit]);

  return (
    <div className="App">
      <h1>Hello Poke API!</h1>
      <div>
        <p>Please enter how many pokemon to load</p>
        <input
          type="number"
          name="limit"
          value={limit}
          onChange={(e) => setLimit(e.target.value)}
        />
      </div>
      {isLoading && <p style={{ color: "blue" }}>Loading ...</p>}
      {error && <p style={{ color: "red" }}>{error}</p>}
      <ul>
        {pokemon.map((p) => (
          <li key={p.name}>{p.name}</li>
        ))}
      </ul>
    </div>
  );
}

```

You can see how we already have more than 50 lines a code for a simple fetch!

We could extract this into a **custom hook** to keep things simple.

## The useFetch hook

The [official docs also suggest using a custom hook](https://react.dev/learn/you-might-not-need-an-effect?ref=reactpractice.dev#fetching-data) if you *really* want to just fetch data by yourself:

![](https://storage.ghost.io/c/7c/fc/7cfc444f-a1c9-4dd3-a87e-8dcc2f1180be/content/images/2025/06/Pasted-image-20250611112257.png)

With a custom hook, this is how our code would look:

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

const useFetch = (url) => {
  const [data, setData] = useState();
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    let ignore = false;

    async function fetchData() {
      setIsLoading(true);
      try {
        const response = await fetch(url);
        const data = await response.json();
        if (!ignore) {
          setData(data);
          setIsLoading(false);
        }
      } catch (e) {
        if (!ignore) {
          setError(e.message);
          setData([]);
          setIsLoading(false);
        }
      }
    }

    fetchData();

    return () => {
      ignore = true;
    };
  }, [url]);

  return { data, isLoading, error };
};

export default function App() {
  const [limit, setLimit] = useState(2);

  const { data, isLoading, error } = useFetch(
    `https://pokeapi.co/api/v2/pokemon?limit=${limit}`
  );
  const pokemon = data?.results || [];

  return (
    <div className="App">
      <h1>Hello Poke API!</h1>
      <div>
        <p>Please enter how many pokemon to load</p>
        <input
          type="number"
          name="limit"
          value={limit}
          onChange={(e) => setLimit(e.target.value)}
        />
      </div>
      {isLoading && <p style={{ color: "blue" }}>Loading ...</p>}
      {error && <p style={{ color: "red" }}>{error}</p>}
      <ul>
        {pokemon.map((p) => (
          <li key={p.name}>{p.name}</li>
        ))}
      </ul>
    </div>
  );
}

```

Much nicer, and it's now also reusable!

**But was it really worth it?** A lot of boilerplate and we don't even have caching!  
If we use this hook in two different components, the data will be fetched twice, instead of read from a cache.

This is why I suggest using [react-query](https://tanstack.com/query/latest/docs/framework/react/overview?ref=reactpractice.dev) by default.

## With react-query

The same example, with react-query, would be as simple as:

```
import { useState } from "react";
import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from "@tanstack/react-query";

const queryClient = new QueryClient();

const PokemonList = () => {
  const [limit, setLimit] = useState(2);

  const { isLoading, error, data } = useQuery({
    queryKey: ["fetchPokemon", limit],
    queryFn: () =>
      fetch(`https://pokeapi.co/api/v2/pokemon?limit=${limit}`).then((res) =>
        res.json()
      ),
  });
  const pokemon = data?.results || [];

  return (
    <div className="App">
      <h1>Hello Poke API!</h1>
      <div>
        <p>Please enter how many pokemon to load</p>
        <input
          type="number"
          name="limit"
          value={limit}
          onChange={(e) => setLimit(e.target.value)}
        />
      </div>
      {isLoading && <p style={{ color: "blue" }}>Loading ...</p>}
      {error && <p style={{ color: "red" }}>{error}</p>}
      <ul>
        {pokemon.map((p) => (
          <li key={p.name}>{p.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <PokemonList />
    </QueryClientProvider>
  );
}

```

Notice how we have built in loading and error states, caching and more!

---

I hope this article highlighted why I think it's no longer worth it to fetch data with `useEffect`, even for simple apps.

You can try out all the examples in this article in this code sandbox: [https://codesandbox.io/p/sandbox/6vr69l](https://codesandbox.io/p/sandbox/6vr69l?ref=reactpractice.dev).