React Query Tutorial: How to Fetch Data Easily in React
useQuery(['key'], fetchFunction) inside your component to fetch data and manage loading and error states automatically.Examples
How to Think About It
Algorithm
Code
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> ); }
Dry Run
Let's trace fetching todos with React Query
Call useQuery
useQuery(['todos'], fetchTodos) starts fetching data
Loading state
isLoading is true, so UI shows 'Loading...'
Data received
Data arrives, isLoading false, error null, data contains todos
| Step | isLoading | error | data |
|---|---|---|---|
| 1 | true | null | undefined |
| 2 | false | null | [{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
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;
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;
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.
| Approach | Time | Space | Best For |
|---|---|---|---|
| React Query | O(1) per fetch | O(n) cache size | Automatic caching and background updates |
| Manual fetch with useEffect | O(1) per fetch | O(1) | Simple cases without caching |
| SWR | O(1) per fetch | O(n) cache size | Simple caching with less config |
QueryClientProvider before using useQuery.QueryClientProvider causes React Query hooks to fail.