React Query vs SWR vs Server Components: Data Fetching in Next.js 14
Next.js 14 with the App Router changed data fetching significantly. Server Components fetch data directly — no hooks, no client-side requests. But React Query and SWR still have a role. Here's when to use each.
Server Components (The Default)
In the App Router, async Server Components are the primary data fetching pattern:
// app/dashboard/page.tsx — Server Component
import { db } from '@/lib/db';
import { auth } from '@/auth';
export default async function Dashboard() {
const session = await auth();
const user = await db.user.findUnique({
where: { id: session!.user.id },
include: { subscription: true },
});
return <DashboardView user={user} />;
}
No useEffect, no loading states, no client-side fetch. The data is fetched on the server, the component renders with it.
Use Server Components when:
- Data is needed for initial render
- Data doesn't change while the user is on the page
- You want minimal client-side JavaScript
- You're accessing the database directly (no API round-trip)
SWR: Lightweight Client-Side Fetching
SWR is for data that needs to stay fresh while the user is on the page:
'use client';
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(r => r.json());
export function LiveStats() {
const { data, error, isLoading } = useSWR('/api/stats', fetcher, {
refreshInterval: 30000, // refresh every 30 seconds
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Failed to load</div>;
return <div>Views today: {data.views}</div>;
}
Use SWR when:
- Data should update automatically (polling)
- Users navigate between pages and data should revalidate on focus
- You need optimistic updates
- The bundle size constraint is important (SWR is ~4kb)
React Query (TanStack Query): Complex Client State
React Query adds more power than SWR: mutations with cache invalidation, infinite scroll, dependent queries, and detailed loading states.
'use client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
export function PostList() {
const queryClient = useQueryClient();
const { data: posts, isLoading } = useQuery({
queryKey: ['posts'],
queryFn: () => fetch('/api/posts').then(r => r.json()),
});
const deletePost = useMutation({
mutationFn: (id: string) => fetch(`/api/posts/${id}`, { method: 'DELETE' }),
onSuccess: () => {
// Invalidate and refetch the posts list
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
if (isLoading) return <PostSkeleton />;
return (
<ul>
{posts.map((post: Post) => (
<li key={post.id}>
{post.title}
<button onClick={() => deletePost.mutate(post.id)}>Delete</button>
</li>
))}
</ul>
);
}
Use React Query when:
- Mutations need to update related cached data
- Infinite scroll / pagination
- Optimistic updates with rollback on failure
- Multiple components share the same data and need sync
- You need detailed control over loading/error/success states
Decision Matrix
| Scenario | Pattern |
|---|---|
| Initial page data (no changes while viewing) | Server Component |
| Data that updates every 30s automatically | SWR with refreshInterval |
| Form submission that updates a list | React Query mutation |
| Dashboard stats (view once) | Server Component |
| Live AI chat messages | API Route + ReadableStream |
| Infinite scroll feed | React Query with useInfiniteQuery |
| Shared cart state across components | React Query or Zustand |
The Hybrid Pattern
Server Components for initial load, client-side fetching for dynamic updates:
// Server Component: fetches initial data, passes to client component
export default async function DashboardPage() {
const initialData = await db.analytics.findMany({ ... });
return <LiveDashboard initialData={initialData} />;
}
// Client Component: uses SWR but seeds with server data to avoid loading flicker
'use client';
export function LiveDashboard({ initialData }: { initialData: Analytics[] }) {
const { data } = useSWR('/api/analytics', fetcher, {
fallbackData: initialData,
refreshInterval: 60000,
});
return <AnalyticsChart data={data} />;
}
No loading state on first render, live updates thereafter.
Pre-Configured in the Starter Kit
The AI SaaS Starter Kit uses this hybrid pattern — Server Components for dashboard data, SWR for live stats, React Query for mutations with cache invalidation.
Atlas — building at whoffagents.com
Top comments (0)