We use cookies to enhance your experience on the site
CodeWorlds

React Query / TanStack Query - Modern Server State Management

On our cosmic journey through React, we have reached a point where managing server data becomes critical. React Query (now TanStack Query) is a revolutionary library that changes how we think about data fetching, caching, and synchronizing server state with the UI. Much like an advanced navigation system that automatically tracks the spaceship's position, React Query automatically manages the state of your data.

Why React Query Instead of useEffect + fetch?

The traditional approach with useEffect and fetch has many problems:

1// Traditional approach - MANY PROBLEMS
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  // Problems:
22  // 1. No caching - same data fetched multiple times
23  // 2. No automatic refetching
24  // 3. No optimistic updates
25  // 4. No request deduplication
26  // 5. No stale data handling
27  // 6. Must manually manage loading/error states
28  // 7. Race condition issues
29
30  if (loading) return <div>Loading...</div>;
31  if (error) return <div>Error: {error.message}</div>;
32  return <div>{user.name}</div>;
33}

React Query solves all these problems:

1// React Query - ELEGANT AND POWERFUL
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>Loading...</div>;
11  if (error) return <div>Error: {error.message}</div>;
12  return <div>{user.name}</div>;
13}

Installation and Basic Configuration

1# Installing TanStack Query (React Query v5)
2npm install @tanstack/react-query
3npm install @tanstack/react-query-devtools  # Optional DevTools
1// App.jsx - Setup QueryClient
2import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
4
5// Create Query Client with configuration
6const queryClient = new QueryClient({
7  defaultOptions: {
8    queries: {
9      staleTime: 1000 * 60 * 5, // 5 minutes
10      cacheTime: 1000 * 60 * 30, // 30 minutes
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 - visible only in 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="Mission name" required />
56      <input name="destination" placeholder="Mission destination" required />
57      <input name="date" type="date" required />
58
59      <button type="submit" disabled={mutation.isPending}>
60        {mutation.isPending ? 'Creating...' : 'Create Mission'}
61      </button>
62    </form>
63  );
64}

Pagination with 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>Loading...</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              Previous
32            </button>
33
34            <span>Page {page} of {data.totalPages}</span>
35
36            <button
37              onClick={() => setPage(old => old + 1)}
38              disabled={isPlaceholderData || page >= data.totalPages}
39            >
40              Next
41            </button>
42          </div>
43        </>
44      )}
45    </div>
46  );
47}

Infinite Queries - Infinite Scrolling

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>Loading...</div>;
20  if (status === 'error') return <div>Loading error</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          ? 'Loading...'
42          : hasNextPage
43          ? 'Load More'
44          : 'No more data'}
45      </button>
46    </div>
47  );
48}

React Query / TanStack Query revolutionizes how we manage server data in React. Just as an autopilot in a spaceship automates navigation and course correction, React Query automates fetching, caching, synchronization, and data updates, allowing us to focus on building great user experiences.

Go to CodeWorlds