React Query is one of the most powerful tools for managing server state in React applications. Instead of manually handling loading states, API requests, caching, re-fetching, and synchronization, React Query gives us a reliable and efficient workflow.
In this guide, we’ll build a Pokémon Browser App that demonstrates:
useInfiniteQueryuseQueryuseMutation + cache updatesThis is an excellent intermediate-level example that will help you truly understand how React Query works.
npm create vite@latest pokemon-react-query --template reactcd pokemon-react-querynpm install
Run it:
npm run dev
npm install @tanstack/react-query @tanstack/react-query-devtools
npm install tailwindcss @tailwindcss/vite
Open your vite.config.js or vite.config.ts and add:
import { defineConfig } from 'vite'import tailwindcss from '@tailwindcss/vite'export default defineConfig({plugins: [tailwindcss(),],})
Add Tailwind to src/index.css:
@import "tailwindcss";
main.jsximport React from 'react';import ReactDOM from 'react-dom/client';import App from './App.jsx';import './index.css';import { QueryClient, QueryClientProvider } from '@tanstack/react-query';import { ReactQueryDevtools } from '@tanstack/react-query-devtools';const queryClient = new QueryClient();ReactDOM.createRoot(document.getElementById('root')).render(<React.StrictMode><QueryClientProvider client={queryClient}><App /><ReactQueryDevtools initialIsOpen={false} /></QueryClientProvider></React.StrictMode>);
useInfiniteQueryThis will load the Pokémon in pages and let the user load more.
// src/PokemonList.tsximport { useInfiniteQuery } from '@tanstack/react-query';interface Pokemon {name: string;url: string;}interface PokemonPage {results: Pokemon[];next: string | null;}const fetchPokemonPage = async ({ pageParam }: { pageParam: string }): Promise<PokemonPage> => {const res = await fetch(pageParam);return res.json();};export function PokemonList({ onSelect }: { onSelect: (name: string) => void }) {const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery<PokemonPage>({queryKey: ['pokemon'],queryFn: fetchPokemonPage,initialPageParam: 'https://pokeapi.co/api/v2/pokemon?limit=20', // ✅ Required in v5getNextPageParam: (lastPage) => lastPage.next,});return (<div className="p-4"><h1 className="text-3xl font-semibold mb-4">Pokémon</h1><ul className="grid grid-cols-2 gap-4">{data?.pages.flatMap(page =>page.results.map((pokemon) => (<likey={pokemon.name}onClick={() => onSelect(pokemon.name)}className="p-3 bg-white shadow cursor-pointer capitalize rounded hover:bg-gray-100">{pokemon.name}</li>)))}</ul><buttononClick={() => fetchNextPage()}disabled={!hasNextPage || isFetchingNextPage}className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50">{isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'No More Pokémon'}</button></div>);}
useQuery// src/PokemonDetail.tsximport { useQuery } from '@tanstack/react-query';interface PokemonDetailData {name: string;sprites: {front_default: string;};}const fetchPokemonDetail = async (name: string): Promise<PokemonDetailData> => {const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`);return res.json();};export function PokemonDetail({ name, onBack }: { name: string; onBack: () => void }) {const { data, isLoading } = useQuery({queryKey: ['pokemon', name],queryFn: () => fetchPokemonDetail(name),});if (isLoading) return <p className="p-4">Loading...</p>;return (<div className="p-4"><button onClick={onBack} className="text-blue-600 underline mb-4">← Back</button><h1 className="text-3xl font-bold capitalize">{data?.name}</h1><img src={data?.sprites.front_default} className="mt-4 w-32" /></div>);}
App.jsximport { useState } from 'react';import { PokemonList } from './PokemonList';import { PokemonDetail } from './PokemonDetail';export default function App() {const [selected, setSelected] = useState(null);return selected ? (<PokemonDetail name={selected} onBack={() => setSelected(null)} />) : (<PokemonList onSelect={setSelected} />);}
npm run dev
You now have:
✅ Infinite list loading ✅ Detail fetching ✅ Cache-friendly navigation ✅ Modern UI with Tailwind ✅ Full React Query DevTools support
This app demonstrates the real value of React Query:
| Feature | Benefit |
|---|---|
| Automatic caching | No redundant requests |
| Background refetching | Stays up-to-date |
| Infinite queries | Works flawlessly |
| Detail query reuse | Cache makes navigation instant |
| Devtools | Debug your data easily |
React Query makes server state manageable, scalable, and elegant.
If you want, I can now continue with: