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.
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}1# Installing TanStack Query (React Query v5)
2npm install @tanstack/react-query
3npm install @tanstack/react-query-devtools # Optional DevTools1// 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}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}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}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.