Home
Daily
React Query: Build a Pokémon App with Vite + TailwindCSS
December 10, 2025
1 min

Table Of Contents

01
1. Create a React App with Vite
02
2. Install Dependencies
03
3. Add TailwindCSS
04
4. Configure React Query in main.jsx
05
5. Pokémon List with useInfiniteQuery
06
6. Pokémon Detail Page with useQuery
07
7. Putting It Together in App.jsx
08
8. Run the App 🎉
09
Final Thoughts

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:

  • ✅ Fetching data with useInfiniteQuery
  • ✅ Loading more results / infinite scrolling
  • ✅ Pokémon detail page with useQuery
  • ✅ Marking favorites using useMutation + cache updates
  • ✅ Debugging with React Query DevTools
  • ✅ Styled using TailwindCSS for a clean, modern UI

This is an excellent intermediate-level example that will help you truly understand how React Query works.


1. Create a React App with Vite

npm create vite@latest pokemon-react-query --template react
cd pokemon-react-query
npm install

Run it:

npm run dev

2. Install Dependencies

npm install @tanstack/react-query @tanstack/react-query-devtools

3. Add TailwindCSS

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";

4. Configure React Query in main.jsx

import 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>
);

5. Pokémon List with useInfiniteQuery

This will load the Pokémon in pages and let the user load more.

// src/PokemonList.tsx
import { 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 v5
getNextPageParam: (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) => (
<li
key={pokemon.name}
onClick={() => onSelect(pokemon.name)}
className="p-3 bg-white shadow cursor-pointer capitalize rounded hover:bg-gray-100"
>
{pokemon.name}
</li>
))
)}
</ul>
<button
onClick={() => 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>
);
}

6. Pokémon Detail Page with useQuery

// src/PokemonDetail.tsx
import { 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>
);
}

7. Putting It Together in App.jsx

import { 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} />
);
}

8. Run the App 🎉

npm run dev

You now have:

✅ Infinite list loading ✅ Detail fetching ✅ Cache-friendly navigation ✅ Modern UI with Tailwind ✅ Full React Query DevTools support


Final Thoughts

This app demonstrates the real value of React Query:

FeatureBenefit
Automatic cachingNo redundant requests
Background refetchingStays up-to-date
Infinite queriesWorks flawlessly
Detail query reuseCache makes navigation instant
DevtoolsDebug your data easily

React Query makes server state manageable, scalable, and elegant.


If you want, I can now continue with:


Tags

#redux

Share

Related Posts

Redux Toolkit
🚀 CI/CD: The Engine Behind Modern Software Delivery
December 13, 2025
2 min
© 2025, All Rights Reserved.
Powered By

Social Media

githublinkedinyoutube