W poprzednim wpisie poznaliśmy podstawy React Query – useQuery do pobierania danych, useMutation do ich modyfikowania, cache, invalidation i optimistic updates. To już ogromny krok naprzód w zarządzaniu stanem serwerowym!
Ale prawdziwe aplikacje mają bardziej złożone wymagania: pagination (stronicowanie – dzielenie dużych list na strony), infinite scroll (nieskończone przewijanie – ładowanie kolejnych danych przy scrollowaniu), prefetching (wstępne pobieranie – ładowanie danych zanim użytkownik ich potrzebuje), placeholderData (dane zastępcze – wyświetlanie poprzednich danych podczas ładowania nowych).
🔍 Zaawansowane wzorce React Query - po co?
Problem bez zaawansowanych wzorców:
❌ Bez pagination: Ładujesz 10,000 użytkowników naraz → wolno, browser lag
❌ Bez infinite scroll: Klikanie "Następna strona" 20 razy → frustrujące UX
❌ Bez prefetching: Każde kliknięcie = loading spinner → czujesz lag
❌ Bez placeholderData: UI migocze przy zmianie strony → nieprzyjemne
Z zaawansowanymi wzorcami:
✅ Pagination - ładujesz po 20 → szybko, płynnie
✅ Infinite scroll - scrollujesz w dół → automatycznie ładuje → jak social media! 📱
✅ Prefetching - następna strona już w cache → zero lagu! ⚡
✅ PlaceholderData - pokazujesz starą stronę podczas ładowania → brak migotania ✨
W tym wpisie nauczymy się zaawansowanych wzorców React Query, które sprawią, że Twoja aplikacja będzie działać płynnie, szybko i profesjonalnie. Będzie dużo praktycznych przykładów – po tym wpisie będziesz potrafił obsłużyć nawet najbardziej wymagające scenariusze!
Pagination – stronicowanie danych
Podstawowe stronicowanie
Najprostszy sposób na stronicowanie to osobny query dla każdej strony.
queryKey: ['users', page] – każda strona ma osobny klucz, więc osobny cache
placeholderData: (previousData) => previousData – podczas ładowania nowej strony, wyświetlamy poprzednią (brak migotania UI)
Przyciski nawigacji z walidacją granic
🔍 placeholderData - po co?
Bez placeholderData:
// Klikniesz "Następna strona"
→ data = undefined (brak cache dla strony 2)
→ UI pokazuje "Ładowanie..."
→ MIGOTANIE! 😵 Lista znika!
Z placeholderData:
// Klikniesz "Następna strona"
→ data = previousData (strona 1)
→ UI pokazuje STARĄ listę (bez migotania!)
→ W tle ładuje stronę 2
→ Jak dostanie → płynnie zamienia na nową ✨
UX jest ZNACZNIE lepsze! Brak migotania = profesjonalna aplikacja! 🎯
🌟 Przykład: Porównanie z/bez placeholderData
// ❌ BEZ placeholderData - migotanie
const { data } = useQuery({
queryKey: ['users', page],
queryFn: fetchUsers,
});
// Zmiana strony 1 → 2:
// 1. data = undefined
// 2. Render: "Ładowanie..." ← MIGOTANIE!
// 3. Fetch zakończony
// 4. data = strona 2
// 5. Render: lista użytkowników
// ✅ Z placeholderData - płynnie
const { data } = useQuery({
queryKey: ['users', page],
queryFn: fetchUsers,
placeholderData: prev => prev,
});
// Zmiana strony 1 → 2:
// 1. data = strona 1 (placeholder)
// 2. Render: stara lista ← BEZ MIGOTANIA!
// 3. Fetch zakończony
// 4. data = strona 2
// 5. Render: nowa lista (płynna zmiana)
Prefetching – wstępne pobieranie następnej strony
Pobierz następną stronę w tle, zanim użytkownik kliknie "Następna".
🔍 Prefetching - magiczny trick na zero lagu!
Bez prefetching:
Jesteś na stronie 1
Klikasz "Następna"
Fetch strony 2 (500ms) → loading spinner 😴
Pokazuje stronę 2
Z prefetching:
Jesteś na stronie 1
React Query w TLE pobiera stronę 2 i zapisuje w cache! 🔮
Klikasz "Następna"
Strona 2 JUŻ W CACHE → instant! ⚡ (0ms!)
Użytkownik czuje: "Wow, ta aplikacja jest BŁYSKAWICZNA!" 🚀
Załaduj kolejne dane automatycznie gdy użytkownik dojedzie do końca listy.
🔍 Intersection Observer - jak działa?
Intersection Observer = API które wykrywa czy element jest widoczny na ekranie.
// Tworzymy "sentinel" (wartownika) na końcu listy:
<div ref={observerTarget}>Loading...</div>
// Intersection Observer obserwuje tego sentinela:
observer.observe(observerTarget)
// Gdy użytkownik scrolluje w dół i sentinel staje się widoczny:
isIntersecting = true → fetchNextPage()
// Nowe posty się ładują i dodają do listy
// Sentinel "spada" w dół (bo lista jest dłuższa)
// Użytkownik scrolluje dalej...
// Sentinel znowu widoczny → fetchNextPage()
// ... i tak w kółko! 🔄
Efekt: Jak Facebook/Twitter/Instagram - scrollujesz w nieskończoność! 📱
import { useInfiniteQuery } from '@tanstack/react-query';
import { useEffect, useRef } from 'react';
function AutoInfinitePostsList() {
const observerTarget = useRef(null);
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 0 }) => fetchPosts(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
initialPageParam: 0,
});
// Intersection Observer – wykrywa gdy element jest widoczny
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
// Jeśli sentinel (wartownik – element na końcu listy) jest widoczny
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage(); // Załaduj następną stronę
}
},
{ threshold: 1.0 } // 100% elementu musi być widoczne
);
const currentTarget = observerTarget.current;
if (currentTarget) {
observer.observe(currentTarget);
}
return () => {
if (currentTarget) {
observer.unobserve(currentTarget);
}
};
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
return (
<div>
{data?.pages.map((page, pageIndex) => (
<div key={pageIndex}>
{page.posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</article>
))}
</div>
))}
{/* Sentinel – element obserwowany przez Intersection Observer */}
<div ref={observerTarget} style={{ height: '20px' }}>
{isFetchingNextPage && <div>Ładowanie kolejnych postów...</div>}
</div>
{!hasNextPage && (
<p>To wszystkie posty!</p>
)}
</div>
);
}
Rezultat: Gdy użytkownik scrolluje w dół i dociera do końca listy, automatycznie ładują się kolejne posty. Bez klikania w żaden przycisk!
🌟 Przykład: Infinite scroll z react-intersection-observer
Przykład: Wchodzisz na chat i widzisz ostatnie 20 wiadomości. Scrollujesz w górę → ładuje poprzednie 20. Scrollujesz w dół → ładuje następne 20. Płynnie! 💬
// Scenariusz:
// 1. Pobierasz stronę 1 (posty 1-10)
// 2. Ktoś dodaje nowy post na początku listy
// 3. Pobierasz stronę 2 (skip=10)
// 4. Ale teraz post #10 stał się postem #11!
// 5. Widzisz post #10 DRUGI RAZ! (duplikat) 😱
// Lub odwrotnie:
// 1. Pobierasz stronę 1
// 2. Ktoś usuwa post #5
// 3. Pobierasz stronę 2 (skip=10)
// 4. Ale teraz pominąłeś post który był #11! 💥
// ?page=2&pageSize=10 -> skip=20, take=10
queryFn: async ({ pageParam = 0 }) => {
const response = await apiClient.get(
`/posts?skip=${pageParam * 10}&take=10`
);
return response.data;
},
getNextPageParam: (lastPage, allPages) => {
// Jeśli zwrócono mniej niż 10, nie ma kolejnych
if (lastPage.length < 10) return undefined;
return allPages.length; // Następna strona
},
Wady: Jeśli dane są dodawane/usuwane, możesz zobaczyć duplikaty lub pominąć rekordy.
Cursor-based (zalecane!)
🔍 Cursor-based - dlaczego lepsze?
Cursor-based: "Daj mi posty AFTER kursora X"
// Strona 1: cursor=null → posty od początku, zwraca cursor="abc123"
// Strona 2: cursor="abc123" → posty AFTER "abc123", zwraca cursor="def456"
// Strona 3: cursor="def456" → posty AFTER "def456"
Zalety - brak duplikatów/pominięć:
// Scenariusz:
// 1. Pobierasz cursor=null → posty 1-10, cursor="abc"
// 2. Ktoś dodaje 5 nowych postów na początku
// 3. Pobierasz cursor="abc" → posty AFTER "abc"
// 4. Dostaniesz posty 11-20 (NIE duplikaty!) ✅
// Cursor ZAWSZE wskazuje konkretny punkt w danych,
// niezależnie od tego co się dzieje z listą!
Zalety: Nie ma duplikatów ani pominiętych rekordów, nawet jeśli dane się zmieniają.
Prefetching – wstępne pobieranie danych
Prefetch przy hover (najechaniu myszą)
🔍 Prefetch przy hover - sekretna broń na błyskawiczną nawigację!
Psychologia użytkownika:
Użytkownik najeżdża myszką na link (~500ms)
Zastanawia się "czy kliknąć?" (~300ms)
Klika (~100ms)
Łącznie: ~900ms ZANIM kliknie!
Twoja szansa: W tych 900ms możesz prefetch danych! Gdy kliknie → już w cache! ⚡
Efekt: Użytkownik czuje że aplikacja reaguje NATYCHMIAST. Magia! 🪄
import { useQueryClient } from '@tanstack/react-query';
function UserListItem({ user }: { user: User }) {
const queryClient = useQueryClient();
function handleMouseEnter() {
// Prefetch danych użytkownika przy hover
queryClient.prefetchQuery({
queryKey: ['user', user.id],
queryFn: () => fetchUserDetails(user.id),
staleTime: 60 * 1000, // Cache na 60 sekund
});
}
return (
<Link to={`/users/${user.id}`}
onMouseEnter={handleMouseEnter} // Prefetch przy hover>
{user.name}
</Link>
);
}
Rezultat: Gdy użytkownik najedzie na link, dane są pobierane w tle. Gdy kliknie link, dane są już w cache – strona ładuje się błyskawicznie!
Prefetch kilku queries naraz
🔍 Kiedy prefetch wiele queries?
Use case: Dashboard - wiesz że użytkownik potrzebuje wielu danych jednocześnie
✅ Prefetch przy montowaniu dashboardu
✅ Wszystkie dane ładują się równolegle w tle
✅ Użytkownik widzi "Ładowanie..." JEDEN raz
✅ Potem wszystko instant! 🚀
function Dashboard() {
const queryClient = useQueryClient();
useEffect(() => {
// Prefetch wszystkich kluczowych danych przy montowaniu
queryClient.prefetchQuery({
queryKey: ['stats'],
queryFn: fetchStats,
});
queryClient.prefetchQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications,
});
queryClient.prefetchQuery({
queryKey: ['recent-activity'],
queryFn: fetchRecentActivity,
});
}, [queryClient]);
return <div>Dashboard...</div>;
}
Initial data – dane początkowe z cache
Użyj danych z jednego query jako initial data dla drugiego.
🔍 initialData - oszczędzaj requesty!
Scenariusz:
Masz listę użytkowników w cache (z poprzedniego query)
Użytkownik klika na konkretnego użytkownika
Zamiast czekać na fetch szczegółów...
...wyciągnij podstawowe info z listy jako initialData! ⚡
Rezultat:
UI pokazuje dane NATYCHMIAST (z listy)
W tle fetch pełnych szczegółów
Jak dostanie → płynnie updatuje
Użytkownik nie widzi loading spinnera! ✨
function UserProfile({ userId }: { userId: number }) {
const queryClient = useQueryClient();
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUserDetails(userId),
initialData: () => {
// Spróbuj znaleźć użytkownika w cache listy użytkowników
const users = queryClient.getQueryData(['users']);
return users?.find(u => u.id === userId);
},
initialDataUpdatedAt: () => {
// Kiedy dane 'users' zostały zaktualizowane?
return queryClient.getQueryState(['users'])?.dataUpdatedAt;
},
});
// Jeśli użytkownik był w liście, wyświetli się natychmiast!
// W tle React Query i tak pobierze pełne dane
return <div>{user?.name}</div>;
}
🌟 Przykład: initialData w praktyce (lista → szczegóły)
3. Cursor-based pagination dla dynamicznych danych
// ✅ Dobrze - brak duplikatów
getNextPageParam: (lastPage) => lastPage.nextCursor,
// ❌ Unikaj offset dla danych często się zmieniających
getNextPageParam: (lastPage, allPages) => allPages.length,
✅ Automatic infinite scroll – ładowanie przy scrollowaniu
✅ Bi-directional scroll – górą i dołem
✅ Cursor vs Offset – które podejście wybrać
✅ Prefetching – wstępne pobieranie danych
✅ initialData – oszczędzanie requestów
✅ placeholderData – brak migotania UI
✅ select – transformacja danych
✅ Query cancellation – anulowanie requestów
✅ 5 Best Practices – profesjonalne wzorce
React Query daje Ci supermoce! Pagination, infinite scroll, prefetching – wszystko działa automatycznie i płynnie. Użytkownicy będą zachwyceni szybkością Twojej aplikacji.
W kolejnym wpisie przejdziemy do autentykacji i autoryzacji – login, rejestracja, JWT tokens, protected routes, refresh tokens. Zbudujemy kompletny system bezpieczeństwa!
Zadanie dla Ciebie
Ulepsz swoją aplikację zaawansowanymi wzorcami:
Dodaj pagination z prefetching następnej strony
Zaimplementuj infinite scroll w minimum jednym miejscu
Dodaj search z debounce i query cancellation
Użyj placeholderData dla płynnych przejść
Dodaj prefetching przy hover na linkach
(Bonus) Użyj select do transformacji danych w min 2 miejscach
🎯 BONUS: Wielki projekt końcowy - Advanced E-commerce
Stwórz zaawansowaną listę produktów wykorzystując WSZYSTKIE wzorce: