Next.js offers multiple data fetching strategies. Here's how to choose and implement the right one.
Server Components (Default)#
1// app/users/page.tsx
2// Server Component - fetches on the server
3async function UsersPage() {
4 const users = await fetchUsers(); // Runs on server
5
6 return (
7 <ul>
8 {users.map(user => (
9 <li key={user.id}>{user.name}</li>
10 ))}
11 </ul>
12 );
13}
14
15async function fetchUsers(): Promise<User[]> {
16 const res = await fetch('https://api.example.com/users', {
17 cache: 'force-cache', // Default - cached indefinitely
18 });
19 return res.json();
20}
21
22// With caching options
23async function fetchData() {
24 // Cached (default)
25 const cached = await fetch(url, { cache: 'force-cache' });
26
27 // Not cached - fresh on every request
28 const fresh = await fetch(url, { cache: 'no-store' });
29
30 // Revalidate every 60 seconds
31 const revalidated = await fetch(url, {
32 next: { revalidate: 60 },
33 });
34
35 // Revalidate on demand with tag
36 const tagged = await fetch(url, {
37 next: { tags: ['users'] },
38 });
39}
40
41export default UsersPage;Dynamic Data#
1// app/posts/[id]/page.tsx
2interface Props {
3 params: { id: string };
4}
5
6// Dynamic route with fresh data
7async function PostPage({ params }: Props) {
8 const post = await fetchPost(params.id);
9
10 return (
11 <article>
12 <h1>{post.title}</h1>
13 <p>{post.content}</p>
14 </article>
15 );
16}
17
18async function fetchPost(id: string): Promise<Post> {
19 const res = await fetch(`https://api.example.com/posts/${id}`, {
20 cache: 'no-store', // Always fresh
21 });
22
23 if (!res.ok) {
24 throw new Error('Failed to fetch post');
25 }
26
27 return res.json();
28}
29
30// Generate static paths
31export async function generateStaticParams() {
32 const posts = await fetchPosts();
33
34 return posts.map((post) => ({
35 id: post.id,
36 }));
37}
38
39export default PostPage;Parallel Data Fetching#
1// app/dashboard/page.tsx
2async function DashboardPage() {
3 // Parallel fetching - faster
4 const [user, posts, notifications] = await Promise.all([
5 fetchUser(),
6 fetchPosts(),
7 fetchNotifications(),
8 ]);
9
10 return (
11 <div>
12 <UserProfile user={user} />
13 <PostList posts={posts} />
14 <NotificationList notifications={notifications} />
15 </div>
16 );
17}
18
19// Or use Suspense for progressive loading
20async function DashboardPageWithSuspense() {
21 return (
22 <div>
23 <Suspense fallback={<UserSkeleton />}>
24 <UserSection />
25 </Suspense>
26 <Suspense fallback={<PostsSkeleton />}>
27 <PostsSection />
28 </Suspense>
29 <Suspense fallback={<NotificationsSkeleton />}>
30 <NotificationsSection />
31 </Suspense>
32 </div>
33 );
34}
35
36async function UserSection() {
37 const user = await fetchUser();
38 return <UserProfile user={user} />;
39}Streaming with Suspense#
1// app/page.tsx
2import { Suspense } from 'react';
3
4export default function Page() {
5 return (
6 <main>
7 <h1>Dashboard</h1>
8
9 {/* These load independently */}
10 <Suspense fallback={<LoadingStats />}>
11 <Stats />
12 </Suspense>
13
14 <Suspense fallback={<LoadingChart />}>
15 <Chart />
16 </Suspense>
17
18 <Suspense fallback={<LoadingTable />}>
19 <DataTable />
20 </Suspense>
21 </main>
22 );
23}
24
25// Each component fetches its own data
26async function Stats() {
27 const stats = await fetchStats(); // Slow query
28 return <StatsDisplay stats={stats} />;
29}
30
31async function Chart() {
32 const data = await fetchChartData(); // Another slow query
33 return <ChartDisplay data={data} />;
34}Client-Side Fetching#
1'use client';
2
3import { useState, useEffect } from 'react';
4import useSWR from 'swr';
5
6// With SWR (recommended)
7function UserPosts({ userId }: { userId: string }) {
8 const { data, error, isLoading, mutate } = useSWR(
9 `/api/users/${userId}/posts`,
10 fetcher
11 );
12
13 if (isLoading) return <Loading />;
14 if (error) return <Error error={error} />;
15
16 return (
17 <div>
18 {data.map(post => <Post key={post.id} post={post} />)}
19 <button onClick={() => mutate()}>Refresh</button>
20 </div>
21 );
22}
23
24const fetcher = (url: string) => fetch(url).then(res => res.json());
25
26// With useEffect (basic)
27function Comments({ postId }: { postId: string }) {
28 const [comments, setComments] = useState<Comment[]>([]);
29 const [loading, setLoading] = useState(true);
30
31 useEffect(() => {
32 async function loadComments() {
33 const res = await fetch(`/api/posts/${postId}/comments`);
34 const data = await res.json();
35 setComments(data);
36 setLoading(false);
37 }
38
39 loadComments();
40 }, [postId]);
41
42 if (loading) return <Loading />;
43
44 return <CommentList comments={comments} />;
45}Server Actions#
1// app/actions.ts
2'use server';
3
4export async function createPost(formData: FormData) {
5 const title = formData.get('title') as string;
6 const content = formData.get('content') as string;
7
8 const post = await db.post.create({
9 data: { title, content },
10 });
11
12 revalidatePath('/posts');
13 redirect(`/posts/${post.id}`);
14}
15
16export async function updatePost(id: string, formData: FormData) {
17 await db.post.update({
18 where: { id },
19 data: {
20 title: formData.get('title') as string,
21 },
22 });
23
24 revalidateTag('posts');
25}
26
27// app/posts/new/page.tsx
28import { createPost } from '@/app/actions';
29
30export default function NewPostPage() {
31 return (
32 <form action={createPost}>
33 <input name="title" required />
34 <textarea name="content" required />
35 <button type="submit">Create Post</button>
36 </form>
37 );
38}Revalidation Strategies#
1// Time-based revalidation
2export const revalidate = 60; // Revalidate every 60 seconds
3
4async function Page() {
5 const data = await fetchData();
6 return <div>{data}</div>;
7}
8
9// On-demand revalidation
10// app/api/revalidate/route.ts
11import { revalidatePath, revalidateTag } from 'next/cache';
12
13export async function POST(request: Request) {
14 const { path, tag, secret } = await request.json();
15
16 if (secret !== process.env.REVALIDATION_SECRET) {
17 return Response.json({ error: 'Invalid secret' }, { status: 401 });
18 }
19
20 if (path) {
21 revalidatePath(path);
22 }
23
24 if (tag) {
25 revalidateTag(tag);
26 }
27
28 return Response.json({ revalidated: true });
29}
30
31// Usage with fetch tags
32async function fetchPosts() {
33 const res = await fetch('https://api.example.com/posts', {
34 next: { tags: ['posts'] },
35 });
36 return res.json();
37}
38
39// Trigger revalidation
40await fetch('/api/revalidate', {
41 method: 'POST',
42 body: JSON.stringify({ tag: 'posts', secret: process.env.REVALIDATION_SECRET }),
43});Loading and Error States#
1// app/posts/loading.tsx
2export default function Loading() {
3 return (
4 <div className="animate-pulse">
5 <div className="h-8 bg-gray-200 rounded w-1/2 mb-4" />
6 <div className="h-4 bg-gray-200 rounded w-full mb-2" />
7 <div className="h-4 bg-gray-200 rounded w-3/4" />
8 </div>
9 );
10}
11
12// app/posts/error.tsx
13'use client';
14
15export default function Error({
16 error,
17 reset,
18}: {
19 error: Error;
20 reset: () => void;
21}) {
22 return (
23 <div>
24 <h2>Something went wrong!</h2>
25 <p>{error.message}</p>
26 <button onClick={() => reset()}>Try again</button>
27 </div>
28 );
29}
30
31// app/posts/not-found.tsx
32export default function NotFound() {
33 return (
34 <div>
35 <h2>Post Not Found</h2>
36 <p>Could not find the requested post.</p>
37 </div>
38 );
39}Best Practices#
Server Components:
✓ Default to Server Components
✓ Use parallel fetching
✓ Leverage Suspense boundaries
✓ Cache appropriately
Client Components:
✓ Use SWR or React Query
✓ Handle loading/error states
✓ Implement optimistic updates
✓ Debounce user input
Caching:
✓ Set appropriate cache times
✓ Use tags for grouped revalidation
✓ Consider ISR for static content
✓ Invalidate on mutations
Conclusion#
Next.js provides flexible data fetching options. Use Server Components for initial data, Suspense for streaming, and client-side fetching for interactive updates. Choose caching strategies based on how often data changes and how fresh it needs to be.