Server Components are the default in the App Router. Because they run on the server, they allow secure and efficient data access.
async:export default async function Page() {const data = await fetch('https://api.vercel.app/blog')const posts = await data.json()return (<ul>{posts.map((post) => (<li key={post.id}>{post.title}</li>))}</ul>)}
fetch responses are not cached by defaultfetch(url, { cache: 'no-store' })
Because Server Components run on the server, you can directly query your database:
import { db, posts } from '@/lib/db'export default async function Page() {const allPosts = await db.select().from(posts)return (<ul>{allPosts.map((post) => (<li key={post.id}>{post.title}</li>))}</ul>)}
No API route required. No client bundle increase. Fully secure.
Sometimes you need to fetch data on the client (e.g., user interactions, polling, or real-time updates).
There are two main approaches:
use() API (for streaming)use() APIInstead of awaiting the data in the Server Component, pass the promise to a Client Component.
//Server Componentimport Posts from '@/app/ui/posts'import { Suspense } from 'react'export default function Page() {const posts = getPosts() // Don’t awaitreturn (<Suspense fallback={<div>Loading...</div>}><Posts posts={posts} /></Suspense>)}
//Client Component'use client'import { use } from 'react'export default function Posts({ posts }) {const allPosts = use(posts)return (<ul>{allPosts.map((post) => (<li key={post.id}>{post.title}</li>))}</ul>)}
Because the component is wrapped in <Suspense>, the fallback UI appears while the promise resolves.
'use client'const fetcher = (url) => fetch(url).then((r) => r.json())export default function BlogPage() {const { data, error, isLoading } = useQuery('https://api.vercel.app/blog',fetcher)if (isLoading) return <div>Loading...</div>if (error) return <div>Error: {error.message}</div>return (<ul>{data.map((post) => (<li key={post.id}>{post.title}</li>))}</ul>)}
These libraries provide advanced features like caching, background revalidation, and refetching.
cache() for Non-Fetch DataWhen using an ORM or database directly:
import { cache } from 'react'import { db, posts, eq } from '@/lib/db'export const getPost = cache(async (id: string) => {return db.query.posts.findFirst({where: eq(posts.id, parseInt(id)),})})
This prevents redundant database queries.
Without streaming, the entire page waits for all data before rendering.
With streaming, HTML is broken into smaller chunks and progressively sent to the client.
There are two main ways to enable streaming.
loading.jsCreate:
app/blog/loading.tsx
export default function Loading() {return <div>Loading...</div>}
When navigating, users instantly see layout + loading state while the page renders.
<Suspense>More granular control:
import { Suspense } from 'react'import BlogList from '@/components/BlogList'import BlogListSkeleton from '@/components/BlogListSkeleton'export default function BlogPage() {return (<div><header><h1>Welcome to the Blog</h1><p>Read the latest posts below.</p></header><main><Suspense fallback={<BlogListSkeleton />}><BlogList /></Suspense></main></div>)}
Only BlogList is streamed. The header renders immediately.
const artist = await getArtist(username)const albums = await getAlbums(username)
The second request waits for the first.
const artistPromise = getArtist(username)const albumsPromise = getAlbums(username)const [artist, albums] = await Promise.all([artistPromise,albumsPromise,])
Both requests start at the same time.
Preloading starts data fetching early to prevent blocking.
const preload = (id: string) => {void getItem(id)}
Call preload(id) before other blocking operations to improve performance.
You can also combine this with React’s cache() and server-only utilities for reusable server-side data access patterns.
Next.js App Router gives us a powerful data-fetching model that blends server performance with client interactivity — and when used correctly, it significantly improves both UX and scalability.
If you’d like, I can: