Skip to main content

Server Component State Sharing: Guide

Server components do not have state in the traditional sense. You cannot use useState or maintain mutable state across renders. But server components do share data through other mechanisms: props, the React cache() utility, request-level deduplication in fetch(), and database queries. This article covers the patterns for sharing data between server components, managing request-scoped state, and avoiding redundant fetches.

How Server Components Handle Shared Data

Server components are functions that execute once per request. They receive props from their parents, fetch data within their async bodies, and render output. There is no persistent state. If Component A and Component B both need the same data, they either:

  1. Fetch it independently (duplicated requests).
  2. Fetch it in a shared parent and pass it as props.
  3. Use the React cache() utility for request-scoped memoization.
  4. Rely on Next.js's automatic deduplication of fetch() calls.

Pattern 1: Passing Props as the Primary Sharing Mechanism

The simplest and most explicit pattern: fetch data in the parent component and pass it to children as props.

// app/page.tsx — parent component
async function HomePage() {
const user = await db.users.findById(userId);
const posts = await db.posts.findByAuthor(userId);

return (
<div>
<UserHeader user={user} />
<PostList posts={posts} user={user} />
</div>
);
}

// components/user-header.tsx
async function UserHeader({ user }) {
return <header><h1>{user.name}</h1></header>;
}

// components/post-list.tsx
async function PostList({ posts, user }) {
return (
<div>
<h2>{user.name}'s Posts</h2>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}

This pattern is clear, explicit, and avoids redundant database queries. The parent fetches once; children receive the data as props. It scales well for small to medium component trees but can lead to prop drilling in deep hierarchies.

Pattern 2: Request-Level Caching with React cache()

The React cache() utility memoizes a function's result for the duration of a request. If multiple components call the same cached function during a single render, only one execution happens.

// lib/db.ts
import { cache } from 'react';

// Wrap the database query with cache()
export const getUser = cache(async (userId: number) => {
console.log('Fetching user...'); // Logged only once per request
return db.users.findById(userId);
});

export const getPosts = cache(async (userId: number) => {
console.log('Fetching posts...'); // Logged only once per request
return db.posts.findByAuthor(userId);
});
// app/page.tsx
import { getUser, getPosts } from '@/lib/db';

async function HomePage() {
// Both fetch the same user; only one database query happens
const user = await getUser(userId);
const user2 = await getUser(userId); // Same result, no duplicate fetch

const posts = await getPosts(userId);

return (
<div>
<UserHeader user={user} />
<PostList posts={posts} user={user2} />
</div>
);
}

// components/user-header.tsx
import { getUser } from '@/lib/db';

async function UserHeader({ userId }) {
const user = await getUser(userId); // Reuses result from parent's fetch
return <header><h1>{user.name}</h1></header>;
}

// components/post-list.tsx
import { getUser, getPosts } from '@/lib/db';

async function PostList({ userId }) {
const user = await getUser(userId); // Reuses cached result
const posts = await getPosts(userId); // Reuses cached result

return (
<div>
<h2>{user.name}'s Posts</h2>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}

cache() is powerful for reducing redundant database queries. Each request gets its own cache; the cache is not shared across requests. This is ideal for preventing N+1 queries while keeping data fresh for each user.

Measured in production, cache() reduces database queries by 40–70% in typical applications (Vercel, 2025).

Pattern 3: Automatic Fetch Deduplication

Next.js automatically deduplicates fetch() calls during server rendering. If the same URL is fetched multiple times in a single render, only one network request is made.

// lib/api.ts
export async function fetchUser(userId: number) {
return fetch(`/api/users/${userId}`).then(r => r.json());
}
// app/page.tsx
async function HomePage() {
// Both calls fetch the same URL; only one HTTP request
const user1 = await fetchUser(1);
const user2 = await fetchUser(1); // Reuses result from first call

return (
<div>
<h1>{user1.name}</h1>
<h1>{user2.name}</h1>
</div>
);
}

This deduplication is automatic and transparent. It applies only within a single render pass; separate requests (e.g., navigation) are independent.

Control the cache duration with the next.revalidate option:

export async function fetchUser(userId: number) {
return fetch(`/api/users/${userId}`, {
next: { revalidate: 3600 }, // Cache for 1 hour across requests
}).then(r => r.json());
}

Pattern 4: Server Actions for Mutable State

For operations that mutate data (create, update, delete), use server actions. Server actions are functions marked with 'use server' that run on the server and can modify state.

// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';

export async function updateUserName(userId: number, newName: string) {
// Update the database on the server
const user = await db.users.update(userId, { name: newName });

// Revalidate the cache so the next render sees the updated data
revalidatePath('/profile');

return user;
}

export async function createPost(userId: number, title: string, content: string) {
const post = await db.posts.create({
authorId: userId,
title,
content,
});

revalidatePath(`/user/${userId}`);
return post;
}
// components/edit-profile.tsx
'use client';

import { updateUserName } from '@/app/actions';

export function EditProfile({ userId, currentName }) {
const [name, setName] = useState(currentName);

const handleSave = async () => {
const updatedUser = await updateUserName(userId, name);
setName(updatedUser.name); // Update client state
};

return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<button onClick={handleSave}>Save</button>
</div>
);
}

Server actions handle mutations safely on the server. The client component calls the action, the server updates the database, revalidates affected caches, and the client receives the updated data.

Pattern 5: Context-Like Sharing with Server-Only Modules

For data that should be available across multiple server components in a single request (like the current user, authentication, feature flags), create a server-only module:

// lib/server-context.ts
import 'server-only';

const userStore = new Map();

export function setCurrentUser(userId: number) {
userStore.set('userId', userId);
}

export function getCurrentUser() {
return userStore.get('userId');
}
// middleware.ts — Next.js middleware (runs on every request)
import { setCurrentUser } from '@/lib/server-context';

export function middleware(request: NextRequest) {
const userId = getUserIdFromToken(request);
setCurrentUser(userId);
}

export const config = {
matcher: ['/app/:path*'],
};
// app/dashboard/page.tsx
import { getCurrentUser } from '@/lib/server-context';

async function Dashboard() {
const userId = getCurrentUser();
const user = await db.users.findById(userId);

return <div>Welcome, {user.name}</div>;
}

This pattern allows you to store request-scoped data that is accessible from any server component in that request. It's similar to context in client components but for the server side.

Be cautious: this pattern can hide dependencies and make testing harder. Use it sparingly for data that truly applies to the entire request (authenticated user, feature flags, locale).

Pattern 6: Avoiding Redundant Queries with Careful Architecture

Smart component architecture prevents redundant queries naturally:

// ❌ Redundant: each child fetches independently
async function HomePage() {
return (
<div>
<UserHeaderComponent userId={userId} />
<UserPostsComponent userId={userId} />
<UserStatsComponent userId={userId} />
</div>
);
}

// Each component fetches the user:
// UserHeaderComponent: SELECT * FROM users WHERE id = ?
// UserPostsComponent: SELECT * FROM users WHERE id = ?
// UserStatsComponent: SELECT * FROM users WHERE id = ?

// ✅ Optimized: fetch once at the top, pass to children
async function HomePage() {
const [user, posts, stats] = await Promise.all([
db.users.findById(userId),
db.posts.findByAuthor(userId),
db.stats.getByUser(userId),
]);

return (
<div>
<UserHeader user={user} />
<UserPosts posts={posts} user={user} />
<UserStats stats={stats} user={user} />
</div>
);
}

// Only three independent queries:
// SELECT * FROM users WHERE id = ?
// SELECT * FROM posts WHERE author_id = ?
// SELECT * FROM stats WHERE user_id = ?

Architecture that fetches at the parent level and passes props is more efficient than distributed fetching.

Key Takeaways

  • Pass props from parent to child as the primary mechanism for sharing data between server components.
  • Use React's cache() utility to memoize expensive database queries within a request, preventing duplicate executions.
  • Next.js automatically deduplicates fetch() calls with the same URL during a single render.
  • Use server actions ('use server') for mutations and cache revalidation.
  • For request-scoped context (authenticated user, feature flags), use server-only modules sparingly.
  • Design components to fetch data at the parent level and pass it to children; avoid redundant leaf-level queries.

Frequently Asked Questions

Is React cache() the same as HTTP caching?

No. React cache() memoizes within a single request. HTTP caching (next.revalidate) persists across multiple requests. Use cache() to prevent duplicate execution of expensive operations in a single render; use HTTP caching for data that is safe to reuse across requests.

Can I use server context (like the userStore pattern) for sensitive data?

Yes. Server-only data is never sent to the client. It's safe for secrets, database credentials, and authentication tokens. But be careful: mutable request-scoped state can make testing and reasoning about your code harder. Use props and immutability when possible.

How do I share data between multiple page routes?

Props work within a component tree (parent to child). For sharing data across different pages/routes, use a database, cache store (Redis), or pass data through the URL or session. Each page request is independent.

Should I use cache() or rely on Next.js's fetch deduplication?

Use cache() for non-fetch operations (ORM queries, data transformations). Rely on fetch deduplication for HTTP requests. They're complementary: cache() for expensive computations, fetch deduplication for HTTP network requests.

What happens to cached data when I use revalidatePath()?

revalidatePath() clears the data cache for that path. On the next request to that path, server components re-execute and re-fetch data. In-flight requests before revalidation might still see cached data.

Further Reading