Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

React Query / TanStack Query - nowoczesne zarządzanie stanem serwerowym

W kosmicznej podróży przez React dotarliśmy do punktu, gdzie zarządzanie danymi serwerowymi staje się kluczowe. React Query (obecnie TanStack Query) to rewolucyjna biblioteka, która zmienia sposób, w jaki myślimy o fetching danych, cachingu i synchronizacji stanu serwerowego z UI. Podobnie jak zaawansowany system nawigacji automatycznie śledzi pozycję statku kosmicznego, React Query automatycznie zarządza stanem twoich danych.

Dlaczego React Query zamiast useEffect + fetch?

Tradycyjne podejście z useEffect i fetch ma wiele problemów:

1// Tradycyjne podejście - WIELE PROBLEMÓW
2function UserProfile({ userId }) {
3  const [user, setUser] = useState(null);
4  const [loading, setLoading] = useState(true);
5  const [error, setError] = useState(null);
6
7  useEffect(() => {
8    setLoading(true);
9    fetch(`/api/users/${userId}`)
10      .then(res => res.json())
11      .then(data => {
12        setUser(data);
13        setLoading(false);
14      })
15      .catch(err => {
16        setError(err);
17        setLoading(false);
18      });
19  }, [userId]);
20
21  // Problemy:
22  // 1. Brak cachingu - te same dane pobierane wielokrotnie
23  // 2. Brak automatycznego refetching
24  // 3. Brak optymistycznych aktualizacji
25  // 4. Brak deduplication requests
26  // 5. Brak obsługi stale data
27  // 6. Trzeba ręcznie zarządzać loading/error states
28  // 7. Problemy z race conditions
29
30  if (loading) return <div>Ładowanie...</div>;
31  if (error) return <div>Błąd: {error.message}</div>;
32  return <div>{user.name}</div>;
33}

React Query rozwiązuje te wszystkie problemy:

1// React Query - ELEGANCKIE I POTĘŻNE
2import { useQuery } from '@tanstack/react-query';
3
4function UserProfile({ userId }) {
5  const { data: user, isLoading, error } = useQuery({
6    queryKey: ['user', userId],
7    queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json())
8  });
9
10  if (isLoading) return <div>Ładowanie...</div>;
11  if (error) return <div>Błąd: {error.message}</div>;
12  return <div>{user.name}</div>;
13}

Instalacja i podstawowa konfiguracja

1# Instalacja TanStack Query (React Query v5)
2npm install @tanstack/react-query
3npm install @tanstack/react-query-devtools  # Opcjonalne DevTools
1// App.jsx - Setup QueryClient
2import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
4
5// Stwórz Query Client z konfiguracją
6const queryClient = new QueryClient({
7  defaultOptions: {
8    queries: {
9      staleTime: 1000 * 60 * 5, // 5 minut
10      cacheTime: 1000 * 60 * 30, // 30 minut
11      refetchOnWindowFocus: true,
12      refetchOnReconnect: true,
13      retry: 3,
14      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
15    },
16  },
17});
18
19function App() {
20  return (
21    <QueryClientProvider client={queryClient}>
22      <YourApp />
23      {/* DevTools - widoczne tylko w development */}
24      <ReactQueryDevtools initialIsOpen={false} />
25    </QueryClientProvider>
26  );
27}

useMutation - POST, PUT, DELETE requests

1import { useMutation, useQueryClient } from '@tanstack/react-query';
2
3async function createMission(newMission) {
4  const response = await fetch('/api/missions', {
5    method: 'POST',
6    headers: { 'Content-Type': 'application/json' },
7    body: JSON.stringify(newMission),
8  });
9
10  if (!response.ok) {
11    throw new Error('Failed to create mission');
12  }
13
14  return response.json();
15}
16
17function CreateMissionForm() {
18  const queryClient = useQueryClient();
19
20  const mutation = useMutation({
21    mutationFn: createMission,
22
23    // Optimistic update
24    onMutate: async (newMission) => {
25      await queryClient.cancelQueries({ queryKey: ['missions'] });
26      const previousMissions = queryClient.getQueryData(['missions']);
27      queryClient.setQueryData(['missions'], (old) => [...old, newMission]);
28      return { previousMissions };
29    },
30
31    // Rollback on error
32    onError: (err, newMission, context) => {
33      queryClient.setQueryData(['missions'], context.previousMissions);
34    },
35
36    // Invalidate and refetch
37    onSettled: () => {
38      queryClient.invalidateQueries({ queryKey: ['missions'] });
39    },
40  });
41
42  const handleSubmit = (e) => {
43    e.preventDefault();
44    const formData = new FormData(e.target);
45
46    mutation.mutate({
47      name: formData.get('name'),
48      destination: formData.get('destination'),
49      date: formData.get('date'),
50    });
51  };
52
53  return (
54    <form onSubmit={handleSubmit}>
55      <input name="name" placeholder="Nazwa misji" required />
56      <input name="destination" placeholder="Cel misji" required />
57      <input name="date" type="date" required />
58
59      <button type="submit" disabled={mutation.isPending}>
60        {mutation.isPending ? 'Tworzenie...' : 'Utwórz misję'}
61      </button>
62    </form>
63  );
64}

Pagination z React Query

1import { useQuery, keepPreviousData } from '@tanstack/react-query';
2
3function PaginatedMissions() {
4  const [page, setPage] = useState(1);
5
6  const { data, isLoading, isPlaceholderData } = useQuery({
7    queryKey: ['missions', page],
8    queryFn: () => fetchMissions(page),
9    placeholderData: keepPreviousData,
10  });
11
12  return (
13    <div>
14      {isLoading ? (
15        <div>Ładowanie...</div>
16      ) : (
17        <>
18          <div className="missions-list">
19            {data.missions.map(mission => (
20              <div key={mission.id} className="mission-item">
21                <h3>{mission.name}</h3>
22              </div>
23            ))}
24          </div>
25
26          <div className="pagination">
27            <button
28              onClick={() => setPage(old => Math.max(old - 1, 1))}
29              disabled={page === 1}
30            >
31              Poprzednia
32            </button>
33
34            <span>Strona {page} z {data.totalPages}</span>
35
36            <button
37              onClick={() => setPage(old => old + 1)}
38              disabled={isPlaceholderData || page >= data.totalPages}
39            >
40              Następna
41            </button>
42          </div>
43        </>
44      )}
45    </div>
46  );
47}

Infinite Queries - nieskończone scrollowanie

1import { useInfiniteQuery } from '@tanstack/react-query';
2
3function InfiniteMissions() {
4  const {
5    data,
6    fetchNextPage,
7    hasNextPage,
8    isFetchingNextPage,
9    status,
10  } = useInfiniteQuery({
11    queryKey: ['infiniteMissions'],
12    queryFn: ({ pageParam = 1 }) => fetch(`/api/missions?page=${pageParam}&limit=20`).then(r => r.json()),
13    initialPageParam: 1,
14    getNextPageParam: (lastPage, allPages) => {
15      return lastPage.hasMore ? allPages.length + 1 : undefined;
16    },
17  });
18
19  if (status === 'pending') return <div>Ładowanie...</div>;
20  if (status === 'error') return <div>Błąd ładowania</div>;
21
22  return (
23    <div>
24      <div className="infinite-list">
25        {data.pages.map((page, i) => (
26          <React.Fragment key={i}>
27            {page.missions.map(mission => (
28              <div key={mission.id} className="mission-card">
29                <h3>{mission.name}</h3>
30              </div>
31            ))}
32          </React.Fragment>
33        ))}
34      </div>
35
36      <button
37        onClick={() => fetchNextPage()}
38        disabled={!hasNextPage || isFetchingNextPage}
39      >
40        {isFetchingNextPage
41          ? 'Ładowanie...'
42          : hasNextPage
43          ? 'Załaduj więcej'
44          : 'Brak więcej danych'}
45      </button>
46    </div>
47  );
48}

React Query / TanStack Query rewolucjonizuje sposób, w jaki zarządzamy danymi serwerowymi w React. Podobnie jak autopilot w statku kosmicznym automatyzuje nawigację i koryguje kurs, React Query automatyzuje fetching, caching, synchronizację i aktualizacje danych, pozwalając nam skupić się na budowaniu świetnych doświadczeń użytkownika.

Przejdź do CodeWorlds