0
0
ReactHow-ToBeginner · 3 min read

React Query Tutorial: How to Fetch Data Easily in React

Use React Query by installing it, then call useQuery(['key'], fetchFunction) inside your component to fetch data and manage loading and error states automatically.
📋

Examples

InputuseQuery(['todos'], () => fetch('/todos').then(res => res.json()))
OutputReturns { data: todosArray, isLoading: false, error: null } after fetching todos
InputuseQuery(['user', userId], () => fetch(`/user/${userId}`).then(res => res.json()))
OutputReturns { data: userObject, isLoading: false, error: null } for given userId
InputuseQuery(['posts'], () => Promise.reject(new Error('Error')))
OutputReturns { data: undefined, isLoading: false, error: Error('Error') } showing fetch failure
🧠

How to Think About It

To fetch data with React Query, think of it as a helper that runs your fetch code and keeps track of loading, success, and error states for you. You give it a unique key and a function that fetches data, and it returns the current status and data, so you can easily show loading spinners or errors in your UI.
📐

Algorithm

1
Install React Query and wrap your app with QueryClientProvider
2
Define a fetch function that returns a promise with your data
3
Call useQuery with a unique key and the fetch function inside your component
4
React Query runs the fetch function and returns loading, error, and data states
5
Use these states to render your UI accordingly
💻

Code

react
import React from 'react';
import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function Todos() {
  const { data, isLoading, error } = useQuery(['todos'], () =>
    fetch('https://jsonplaceholder.typicode.com/todos').then(res => res.json())
  );

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error loading todos</p>;

  return (
    <ul>
      {data.slice(0, 5).map(todo => <li key={todo.id}>{todo.title}</li>)}
    </ul>
  );
}

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Todos />
    </QueryClientProvider>
  );
}
Output
Loading... (briefly), then a list of 5 todo titles from the API
🔍

Dry Run

Let's trace fetching todos with React Query

1

Call useQuery

useQuery(['todos'], fetchTodos) starts fetching data

2

Loading state

isLoading is true, so UI shows 'Loading...'

3

Data received

Data arrives, isLoading false, error null, data contains todos

StepisLoadingerrordata
1truenullundefined
2falsenull[{id:1,title:'...'}, ...]
💡

Why This Works

Step 1: useQuery runs fetch function

React Query calls your fetch function automatically when the component mounts or the key changes.

Step 2: Manages loading and error states

It tracks if the data is loading or if there was an error, so you don't have to write that logic yourself.

Step 3: Returns data for rendering

Once data is fetched, React Query provides it so you can display it in your UI easily.

🔄

Alternative Approaches

Using fetch with useEffect and useState
react
import React, { useState, useEffect } from 'react';

function Todos() {
  const [todos, setTodos] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/todos')
      .then(res => res.json())
      .then(data => {
        setTodos(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, []);

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

  return <ul>{todos.slice(0, 5).map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>;
}

export default Todos;
More manual work to handle loading and errors; less automatic caching and refetching.
Using SWR library
react
import useSWR from 'swr';

const fetcher = url => fetch(url).then(res => res.json());

function Todos() {
  const { data, error } = useSWR('https://jsonplaceholder.typicode.com/todos', fetcher);

  if (!data) return <p>Loading...</p>;
  if (error) return <p>Error loading todos</p>;

  return <ul>{data.slice(0, 5).map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>;
}

export default Todos;
SWR is simpler but has fewer features than React Query, like less control over caching.

Complexity: O(1) per fetch call time, O(n) for cached data size space

Time Complexity

Each fetch is a network call which is constant time per request; React Query manages caching to avoid unnecessary calls.

Space Complexity

React Query stores fetched data in cache, so space grows with number of unique queries.

Which Approach is Fastest?

React Query is faster in UI responsiveness due to caching and background updates compared to manual fetch with useEffect.

ApproachTimeSpaceBest For
React QueryO(1) per fetchO(n) cache sizeAutomatic caching and background updates
Manual fetch with useEffectO(1) per fetchO(1)Simple cases without caching
SWRO(1) per fetchO(n) cache sizeSimple caching with less config
💡
Always wrap your app with QueryClientProvider before using useQuery.
⚠️
Forgetting to wrap your app in QueryClientProvider causes React Query hooks to fail.