W poprzednim wpisie nauczyliśmy się komunikować z API używając Axios i Fetch. Ale zauważyłeś pewnie, że za każdym razem musimy ręcznie zarządzać stanem loading (ładowanie), error (błąd), data (dane), refetch (ponowne pobieranie), cache (pamięć podręczna – zapisane wcześniej pobrane dane). To dużo boilerplate (powtarzalnego kodu) i łatwo o błędy.
🔍 Problem z tradycyjnym fetching:
Bez React Query, każdy API call to ~20 linii kodu:
❌ Brak cache - każde odwiedzenie komponentu = nowy request
❌ Brak retry - błąd = koniec
❌ Brak refetch - stare dane zostają
❌ Trudne testowanie
React Query (obecnie nazywany TanStack Query) rozwiązuje wszystkie te problemy. To biblioteka, która automatycznie zarządza stanem serwerowym – danymi pobieranymi z API. Daje cache, automatyczne refetch, synchronizację między tabami przeglądarki, optimistic updates (optymistyczne aktualizacje – zaktualizuj UI przed otrzymaniem odpowiedzi z serwera, zakładając że operacja się powiedzie) i wiele więcej. To zmienia sposób myślenia o danych w React.
🔍 Co to jest "stan serwerowy" vs "stan klienta"?
Stan klienta = dane które żyją TYLKO w przeglądarce:
Czy modal jest otwarty? (true/false)
Aktualny tab w nawigacji
Wartości w formularzu (przed wysłaniem)
Stan serwerowy = dane które pochodzą z API:
Lista użytkowników z bazy danych
Dane profilu użytkownika
Produkty w sklepie
React Query zarządza TYLKO stanem serwerowym! Dla stanu klienta używaj useState/Zustand. 🎯
W tym wpisie poznamy podstawy React Query – queries (zapytania do API), mutations (operacje zmieniające dane), cache, refetch strategies (strategie odświeżania danych). To będzie praktyczny wpis z wieloma wyjaśnieniami – po nim nie będziesz już ręcznie zarządzał stanem API calls!
Obuduj app w QueryClientProvider - udostępnia QueryClient wszystkim komponentom
Dodaj ReactQueryDevtools - panel do debugowania (opcjonalne, ale polecam!)
// main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
// Utwórz QueryClient - konfiguracja React Query dla całej aplikacji
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 60 sekund - jak długo dane są uważane za "świeże"
retry: 1, // Liczba ponownych prób przy błędzie
},
},
});
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
{/* DevTools - panel do debugowania, widoczny tylko w development */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</React.StrictMode>
);
QueryClientProvider opakowuje całą aplikację i dostarcza dostęp do React Query we wszystkich komponentach.
🔍 Co to jest staleTime?
staleTime = jak długo dane są "świeże" (fresh)?
Fresh (świeże) - dane są aktualne, NIE refetch automatycznie
Stale (nieświeże) - dane mogą być przestarzałe, refetch w tle
Przykład z staleTime: 60000 (60 sekund):
// t=0s: Pierwszy render komponentu
→ Fetch z API (brak cache)
→ Dane są FRESH przez 60s
// t=10s: Drugi render tego samego komponentu
→ NIE fetchuje! Dane są fresh (minęło 10s < 60s)
→ Pokazuje z cache ⚡
// t=70s: Trzeci render
→ Dane są STALE (minęło 70s > 60s)
→ Pokazuje stare z cache, ale refetch w tle
→ Jak dostanie nowe dane, aktualizuje UI
Krótki staleTime (5s) = częste refetch (zawsze aktualne dane, więcej requestów) Długi staleTime (5min) = rzadkie refetch (może być nieaktualne, mniej requestów)
🌟 Przykład: Różne staleTime dla różnych danych
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // Domyślnie 5 minut
},
},
});
// W komponencie możesz nadpisać:
// Dane które się rzadko zmieniają (lista krajów)
useQuery({
queryKey: ['countries'],
queryFn: fetchCountries,
staleTime: 24 * 60 * 60 * 1000, // 24 godziny! Kraje się nie zmieniają
});
// Dane które się często zmieniają (cena akcji)
useQuery({
queryKey: ['stock-price'],
queryFn: fetchStockPrice,
staleTime: 10 * 1000, // 10 sekund - zawsze aktualne!
});
// Dane które NIE mogą być stale (koszyk użytkownika)
useQuery({
queryKey: ['cart'],
queryFn: fetchCart,
staleTime: 0, // Zawsze stale = zawsze refetch!
});
useQuery - pobieranie danych
useQuery to hook do pobierania danych z API (operacje read-only, tylko czytanie).
Tablica pozwala dodawać parametry! Każda kombinacja = osobny cache:
// 3 różne query keys = 3 osobne cache
['user', 1] → cache dla użytkownika 1
['user', 2] → cache dla użytkownika 2
['user', 5] → cache dla użytkownika 5
// Każdy ma swoje dane w cache!
// Gdy zmienisz userId z 1 na 2:
// → React Query widzi nowy key ['user', 2]
// → Sprawdza cache
// → Nie ma? Fetch z API
// → Ma? Pokaż z cache ⚡
🌟 Przykład: Query Keys w praktyce
// ✅ DOBRY - parametry w query key
function UserProfile({ userId }) {
const { data } = useQuery({
queryKey: ['user', userId], // Każdy user ma osobny cache!
queryFn: () => fetchUser(userId)
});
}
// Scenariusz:
// 1. userId = 1 → fetch user 1 → cache jako ['user', 1]
// 2. userId = 2 → fetch user 2 → cache jako ['user', 2]
// 3. userId = 1 → pokazuje z cache! (już był!) ⚡
// ❌ ZŁY - parametr NIE w query key
function UserProfile({ userId }) {
const { data } = useQuery({
queryKey: ['user'], // Zawsze ten sam key!
queryFn: () => fetchUser(userId)
});
}
// Scenariusz:
// 1. userId = 1 → fetch user 1 → cache jako ['user']
// 2. userId = 2 → React Query widzi ten sam key ['user']
// 3. Myśli że ma dane! Pokazuje user 1 zamiast user 2! 💥
// BUG!
💪 Ćwiczenie 1: Użyj useQuery
Stwórz komponent PostsList który:
Pobiera posty z /api/posts
Używa useQuery z kluczem ['posts']
Pokazuje loading podczas ładowania
Pokazuje błąd jeśli fetch failuje
Wyświetla listę postów (title, body)
Bonus: Dodaj staleTime: 2 * 60 * 1000 (2 minuty) - posty rzadko się zmieniają!
Status i flags (flagi statusu)
🔍 isLoading vs isFetching - jaka różnica?
To najczęstsza pułapka dla początkujących!
isLoading = true TYLKO gdy:
Pierwsze ładowanie (brak cache)
Loading spinner = isLoading
isFetching = true gdy:
Pierwsze ładowanie (jak isLoading)
+ Refetch w tle (masz cache, ale pobiera nowe dane)
Background refresh indicator = isFetching
// Scenariusz:
// 1. Pierwsze otwarcie komponentu
isLoading = true // Brak cache, pokazujemy spinner
isFetching = true
// 2. Dostaliśmy dane, zapisaliśmy w cache
isLoading = false // Mamy dane!
isFetching = false
// 3. Użytkownik wrócił do zakładki (refetchOnWindowFocus)
isLoading = false // Mamy cache! Pokazujemy dane
isFetching = true // Ale refetch w tle (małe loading w rogu)
// 4. Dostaliśmy nowe dane
isLoading = false
isFetching = false
const {
data, // Dane z API lub cache
error, // Obiekt błędu (jeśli wystąpił)
// Status
status, // 'pending' | 'error' | 'success'
// Boolean flags (flagi logiczne) - wygodniejsze od status
isLoading, // Pierwsze ładowanie (brak cache)
isFetching, // Pobieranie (nawet jeśli jest cache)
isError, // Czy wystąpił błąd
isSuccess, // Czy sukces
// Dodatkowe
isPending, // === isLoading
refetch, // Funkcja do ręcznego refetch
fetchStatus, // 'fetching' | 'paused' | 'idle'
} = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
Różnica isLoading vs isFetching:
isLoading – true tylko przy pierwszym ładowaniu (brak cache)
isFetching – true zawsze gdy pobiera dane (nawet z cache)
🌟 Przykład: Użycie isLoading vs isFetching w UI
function UserList() {
const { data, isLoading, isFetching } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return (
<div>
{/* isLoading - pełny loading screen (brak cache) */}
{isLoading && (
<div className="full-page-spinner">
<Spinner />
<p>Ładowanie użytkowników...</p>
</div>
)}
{/* isFetching - małe loading (mamy cache, refetch w tle) */}
{isFetching && !isLoading && (
<div className="background-refresh">
<small>Odświeżanie...</small>
</div>
)}
{/* Dane (pokazujemy nawet podczas isFetching!) */}
{data && (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
);
}
// UX:
// 1. Pierwsze ładowanie → pełny spinner (isLoading)
// 2. Mamy dane → lista użytkowników
// 3. Refetch → lista + małe "odświeżanie..." (isFetching)
TypeScript w useQuery
interface User {
id: number;
name: string;
email: string;
}
// Automatyczne typowanie
const { data } = useQuery({
queryKey: ['users'],
queryFn: async (): Promise<User[]> => {
const response = await apiClient.get<User[]>('/users');
return response.data;
},
});
// data ma typ: User[] | undefined
// Lub z generic
const { data } = useQuery<User[]>({
queryKey: ['users'],
queryFn: async () => {
const response = await apiClient.get<User[]>('/users');
return response.data;
},
});
Query Options - opcje konfiguracyjne
staleTime - czas "świeżości" danych
staleTime określa jak długo dane są uważane za świeże (fresh). Świeże dane nie są ponownie pobierane.
0 (default) – dane stale się natychmiast po pobraniu (zawsze refetch)
Infinity – dane nigdy nie stale się (nigdy nie refetch automatycznie)
cacheTime (od v5: gcTime) - czas życia cache
gcTime (garbage collection time) określa jak długo cache jest przechowywany po odmontowaniu komponentu.
🔍 staleTime vs gcTime - co jest czym?
staleTime = kiedy dane są "nieświeże" (kiedy refetch) gcTime = kiedy usunąć cache (po odmontowaniu)
// Scenariusz z staleTime: 60s, gcTime: 5min
// t=0s: Komponent montuje się
→ Fetch z API
→ Cache: { users: [...data] }
// t=30s: Komponent montuje się ponownie
→ Dane fresh (30s < 60s staleTime)
→ Pokazuje z cache, NIE refetch
// t=90s: Komponent montuje się ponownie
→ Dane stale (90s > 60s staleTime)
→ Pokazuje z cache, ALE refetch w tle
// t=100s: Komponent się odmontowuje
→ Cache ZOSTAJE przez 5min (gcTime)
// t=200s: Komponent montuje się ponownie
→ Cache NADAL istnieje! (200s < 5min gcTime)
→ Pokazuje cache, refetch w tle
// t=400s: (5min od odmontowania)
→ Cache usunięty (garbage collected)
→ Następne montowanie = fetch z API
mutation.mutate(data);
console.log('To wykona się natychmiast!');
// NIE czeka na wynik
mutateAsync() - zwraca Promise (możesz await):
await mutation.mutateAsync(data);
console.log('To wykona się PO zakończeniu mutation');
// Czeka na wynik
Kiedy co?
✅ mutate() - większość przypadków (prostsze!)
✅ mutateAsync() - gdy potrzebujesz czekać (np. redirect po sukcesie)
Mutation status i flags
const mutation = useMutation({ mutationFn: createUser });
const {
mutate, // Funkcja do wywołania mutation
mutateAsync, // Async wersja (zwraca Promise)
data, // Dane zwrócone przez mutation
error, // Błąd (jeśli wystąpił)
status, // 'idle' | 'pending' | 'error' | 'success'
isPending, // Czy mutation jest w trakcie
isError, // Czy wystąpił błąd
isSuccess, // Czy sukces
isIdle, // Czy nie rozpoczęta
reset, // Reset mutation do stanu początkowego
} = mutation;
Callbacks - funkcje wywoływane w różnych momentach
🔍 Lifecycle useMutation - kolejność callbacków:
mutate(data) - wywołujesz mutation
onMutate(data) - zanim wyślesz request (optimistic update!)
mutationFn(data) - wysyłasz request do API
Jeśli sukces:
onSuccess(result, data, context)
Jeśli błąd:
onError(error, data, context)
onSettled(result, error, data, context) - zawsze na końcu
const mutation = useMutation({
mutationFn: createUser,
onSuccess: (data, variables, context) => {
// Wywołane po sukcesie
console.log('Sukces!', data);
toast.success('Użytkownik utworzony!');
},
onError: (error, variables, context) => {
// Wywołane przy błędzie
console.error('Błąd!', error);
toast.error('Nie udało się utworzyć użytkownika');
},
onSettled: (data, error, variables, context) => {
// Wywołane zawsze (sukces lub błąd)
console.log('Mutation zakończona');
},
});
Invalidation - unieważnienie cache
Po stworzeniu/edycji/usunięciu danych, musisz odświeżyć listę.
React Query zmienia sposób myślenia o danych w React. Zamiast zarządzać stanem ręcznie, deklarujesz co chcesz pobrać, a React Query zajmuje się resztą!
W kolejnym wpisie poznamy część II React Query – pagination (stronicowanie), infinite scroll (nieskończone przewijanie), prefetching (wstępne pobieranie), hydration (napełnianie cache), zaawansowane wzorce!
Zadanie dla Ciebie
Dodaj React Query do swojej aplikacji:
Skonfiguruj QueryClient i Provider
Zamień useState + useEffect na useQuery (min 3 miejsca)
Użyj useMutation dla create/update/delete
Dodaj invalidation po mutations
Zaimplementuj optimistic update dla jednej operacji
(Bonus) Dodaj staleTime i refetch strategies
🎯 BONUS: Wielki projekt końcowy - Blog z React Query
Stwórz kompletny blog używając React Query:
Features:
✅ Lista postów (useQuery + pagination)
✅ Szczegóły posta (useQuery z dynamic ID)
✅ Tworzenie posta (useMutation + invalidation)
✅ Edycja posta (useMutation + optimistic update)
✅ Usuwanie posta (useMutation + invalidation)
✅ Komentarze (dependent queries - post → comments)
✅ Like/Unlike (useMutation + optimistic update)
Config:
staleTime: 2 min (posty rzadko się zmieniają)
refetchOnWindowFocus: true
Query keys factory pattern
DevTools enabled
Bonus challenges:
🏆 Search z debounce i cancel
🏆 Filtry (status, author) z query keys
🏆 Offline support (retry + error handling)
🏆 Loading skeletons (isLoading vs isFetching)
To production-ready aplikacja! Po ukończeniu tego projektu będziesz ekspertem React Query! 🚀✨