Back to Blog
Next.jsReactData FetchingSSR

Next.js Data Fetching Strategies

Fetch data effectively in Next.js. From Server Components to SSR to ISR to client-side patterns.

B
Bootspring Team
Engineering
June 28, 2022
6 min read

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.

Share this article

Help spread the word about Bootspring