nextjs-server-client-components by wsimmonds/claude-nextjs-skills
npx skills add https://github.com/wsimmonds/claude-nextjs-skills --skill nextjs-server-client-components为 Next.js App Router 中服务端组件与客户端组件的选择提供全面指导,包括 cookie/header 访问、searchParams 处理、pathname 路由以及 React 的 'use' API 用于 Promise 解包。
any 类型关键规则: 此代码库已启用 @typescript-eslint/no-explicit-any。使用 any 将导致构建失败。
❌ 错误:
function handleSubmit(e: any) { ... }
const data: any[] = [];
✅ 正确:
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }
const data: string[] = [];
// 页面属性
function Page({ params }: { params: { slug: string } }) { ... }
function Page({ searchParams }: { searchParams: { [key: string]: string | string[] | undefined } }) { ... }
// 表单事件
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { ... }
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { ... }
// 服务端操作
async function myAction(formData: FormData) { ... }
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
在以下情况下使用此技能:
App Router 中的所有组件默认都是服务端组件。 无需指令。
// app/components/ProductList.tsx
// 这是一个服务端组件(默认)
export default async function ProductList() {
const products = await fetch('https://api.example.com/products');
const data = await products.json();
return (
<ul>
{data.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
何时使用服务端组件:
优势:
在文件顶部添加 'use client' 指令以使其成为客户端组件。
// app/components/Counter.tsx
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
何时使用客户端组件:
客户端组件的要求:
'use client' 指令服务端组件是默认的。除非你明确需要客户端功能,否则不要添加 'use client'。
✅ 正确 - 带导航的服务端组件:
// app/page.tsx - 服务端组件(不需要 'use client'!)
import Link from 'next/link';
import { redirect } from 'next/navigation';
export default async function Page() {
// 服务端组件可以是异步的
const data = await fetchData();
if (!data) {
redirect('/login'); // 服务端重定向
}
return (
<div>
<Link href="/dashboard">前往仪表板</Link>
<p>{data.content}</p>
</div>
);
}
❌ 错误 - 向服务端组件添加 'use client':
// app/page.tsx
'use client'; // ❌ 错误!不要向服务端组件添加这个!
export default async function Page() { // ❌ 会失败 - 不允许异步客户端组件
const data = await fetchData();
return <div>{data.content}</div>;
}
服务端导航方法(不需要 'use client'):
next/link 中的 <Link> 组件next/navigation 中的 redirect() 函数客户端导航方法(需要 'use client'):
next/navigation 中的 useRouter() 钩子usePathname() 钩子useSearchParams() 钩子(也需要 Suspense)在服务端组件中使用 next/headers 读取 cookies:
// app/dashboard/page.tsx
import { cookies } from 'next/headers';
export default async function Dashboard() {
const cookieStore = await cookies();
const token = cookieStore.get('session-token');
if (!token) {
redirect('/login');
}
const user = await fetchUser(token.value);
return <div>Welcome, {user.name}</div>;
}
重要说明:
cookies() 必须被 await// app/api/route.ts 或任何服务端组件
import { headers } from 'next/headers';
export default async function Page() {
const headersList = await headers();
const userAgent = headersList.get('user-agent');
const referer = headersList.get('referer');
return <div>User Agent: {userAgent}</div>;
}
在服务端组件中直接访问 URL 查询参数:
// app/search/page.tsx
export default async function SearchPage({
searchParams,
}: {
searchParams: { q?: string; category?: string };
}) {
const query = searchParams.q || '';
const category = searchParams.category || 'all';
const results = await searchProducts(query, category);
return (
<div>
<h1>Search Results for: {query}</h1>
<p>Category: {category}</p>
<ProductList products={results} />
</div>
);
}
重要说明:
searchParams 仅在 page.tsx 文件中可用searchParams 必须被 awaitlayout.tsx 中不可用useSearchParams() 钩子⚠️ 关键警告 - Next.js 15+ searchParams: 在 Next.js 15+ 中提取参数时,必须使用解构来保持 searchParams 标识符与参数提取在同一行可见。不要使用像 params 或 resolved 这样的中间变量——这是一种破坏代码可读性和测试模式的反模式。
异步 searchParams(Next.js 15+):
// app/search/page.tsx (Next.js 15+)
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q?: string }>;
}) {
// 最佳实践:内联访问使 searchParams 和参数保持在同一行
const q = (await searchParams).q || '';
return <div>Search: {q}</div>;
}
关键模式要求:
从 searchParams 提取参数时,始终使用内联访问以保持 searchParams 和参数名称在同一行:
// ✅ 正确:内联访问(必需模式)
const name = (await searchParams).name || '';
// ✅ 也正确:多个参数
const category = (await searchParams).category || 'all';
const sort = (await searchParams).sort || 'asc';
// ❌ 错误:使用中间变量将 searchParams 与参数分离
const params = await searchParams; // 不要这样做
const name = params.name; // 这里看不到 searchParams
// ❌ 错误:解构(searchParams 和 name 在同一行但缺少第二个 'name')
const { name } = await searchParams; // 不推荐
为什么使用内联访问:
searchParams 标识符与参数提取在同一行可见在服务端组件中(page.tsx):
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: { slug: string };
}) {
// params 包含路由参数
const post = await getPost(params.slug);
return <article>{post.title}</article>;
}
异步 params(Next.js 15+):
// app/blog/[slug]/page.tsx (Next.js 15+)
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.title}</article>;
}
在客户端组件中:
使用 next/navigation 中的钩子:
// app/components/Breadcrumbs.tsx
'use client';
import { usePathname, useParams, useSearchParams } from 'next/navigation';
export default function Breadcrumbs() {
const pathname = usePathname(); // 当前路径:/blog/hello-world
const params = useParams(); // 路由参数:{ slug: 'hello-world' }
const searchParams = useSearchParams(); // 查询参数
return (
<nav>
<span>Current path: {pathname}</span>
<span>Slug: {params.slug}</span>
<span>Search: {searchParams.get('q')}</span>
</nav>
);
}
使用 useSearchParams() 钩子时,必须:
'use client' 指令这是 Next.js 要求——不满足这两点将导致错误。
✅ 正确模式:
// app/page.tsx 或任何父组件
import { Suspense } from 'react';
import SearchComponent from './SearchComponent';
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SearchComponent />
</Suspense>
);
}
// app/SearchComponent.tsx
'use client';
import { useSearchParams } from 'next/navigation';
export default function SearchComponent() {
const searchParams = useSearchParams();
const query = searchParams.get('q');
return <div>Search query: {query}</div>;
}
❌ 错误 - 缺少 'use client':
// 这会失败 - useSearchParams 需要 'use client'
import { useSearchParams } from 'next/navigation';
export default function SearchComponent() {
const searchParams = useSearchParams(); // 错误!
return <div>{searchParams.get('q')}</div>;
}
❌ 错误 - 缺少 Suspense 包装器:
// 这会导致问题 - useSearchParams 需要 Suspense
export default function Page() {
return <SearchComponent />; // 缺少 Suspense 包装器!
}
React use API 允许在服务端和客户端组件中读取 Promise 和上下文。
// app/components/UserProfile.tsx
'use client';
import { use } from 'react';
// 重要:使用特定类型、泛型类型或 'unknown' - 绝不使用 'any'
// 选项 1:特定类型(当类型已知时最佳)
export default function UserProfile({
userPromise
}: {
userPromise: Promise<{ name: string; email: string }>
}) {
// 解包 Promise
const user = use(userPromise);
return <div>{user.name}</div>;
}
// 选项 2:泛型类型(用于可重用组件)
export function GenericDataDisplay<T>({
data
}: {
data: Promise<T>
}) {
const result = use(data);
return <div>{JSON.stringify(result)}</div>;
}
// 选项 3:Unknown 类型(当类型真正未知时)
export function UnknownDataDisplay({
data
}: {
data: Promise<unknown>
}) {
const result = use(data);
return <div>{JSON.stringify(result)}</div>;
}
传递 Promise 的服务端组件:
// app/profile/page.tsx
import UserProfile from './components/UserProfile';
export default function ProfilePage() {
// 创建 Promise 但不 await
const userPromise = fetchUser();
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
'use client';
import { use } from 'react';
import { ThemeContext } from './ThemeContext';
export default function ThemedButton() {
const theme = use(ThemeContext);
return <button className={theme.buttonClass}>Click me</button>;
}
// app/products/page.tsx (服务端组件)
import ProductGrid from './ProductGrid';
export default async function ProductsPage() {
const products = await fetchProducts();
// 将数据传递给客户端组件
return <ProductGrid products={products} />;
}
// app/products/ProductGrid.tsx (客户端组件)
'use client';
import { useState } from 'react';
export default function ProductGrid({
products
}: {
products: Product[]
}) {
const [filter, setFilter] = useState('all');
const filtered = products.filter(p =>
filter === 'all' || p.category === filter
);
return (
<div>
<select onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="electronics">Electronics</option>
</select>
{filtered.map(p => <div key={p.id}>{p.name}</div>)}
</div>
);
}
// app/dashboard/page.tsx
export default async function Dashboard() {
// 并行获取
const [user, stats, notifications] = await Promise.all([
fetchUser(),
fetchStats(),
fetchNotifications(),
]);
return (
<div>
<UserInfo user={user} />
<Stats data={stats} />
<Notifications items={notifications} />
</div>
);
}
// app/page.tsx
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading stats...</div>}>
<Stats />
</Suspense>
<Suspense fallback={<div>Loading feed...</div>}>
<Feed />
</Suspense>
</div>
);
}
async function Stats() {
const data = await fetchStats(); // 慢查询
return <div>{data.total}</div>;
}
async function Feed() {
const items = await fetchFeed(); // 快查询
return <ul>{items.map(i => <li key={i.id}>{i.title}</li>)}</ul>;
}
你可以将服务端组件作为子元素传递给客户端组件:
// app/page.tsx (服务端组件)
import ClientWrapper from './ClientWrapper';
import ServerContent from './ServerContent';
export default function Page() {
return (
<ClientWrapper>
{/* 服务端组件作为子元素 */}
<ServerContent />
</ClientWrapper>
);
}
// ClientWrapper.tsx (客户端组件)
'use client';
import { useState } from 'react';
export default function ClientWrapper({
children
}: {
children: React.ReactNode
}) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && children}
</div>
);
}
// ServerContent.tsx (服务端组件)
export default async function ServerContent() {
const data = await fetchData();
return <div>{data.content}</div>;
}
错误:
// app/components/Header.tsx
'use client'; // 不必要!
export default function Header() {
return <header><h1>My App</h1></header>;
}
正确:
// app/components/Header.tsx
// 不需要指令 - 保持为服务端组件
export default function Header() {
return <header><h1>My App</h1></header>;
}
原因: 仅在实际需要客户端功能时使用 'use client'。静态组件应保持为服务端组件以减少包体积。
错误:
'use client';
import { useState, useEffect } from 'react';
export default function Products() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('/api/products')
.then(r => r.json())
.then(setProducts);
}, []);
return <div>{products.map(p => <div key={p.id}>{p.name}</div>)}</div>;
}
正确:
// 服务端组件 - 没有 'use client'
export default async function Products() {
const response = await fetch('https://api.example.com/products');
const products = await response.json();
return <div>{products.map(p => <div key={p.id}>{p.name}</div>)}</div>;
}
原因: 服务端组件可以直接获取数据,消除了加载状态并减少了客户端 JavaScript。
错误:
'use client';
import { cookies } from 'next/headers'; // 错误!
export default function ClientComponent() {
const cookieStore = cookies(); // 这会失败
return <div>...</div>;
}
正确:
// 服务端组件
import { cookies } from 'next/headers';
import ClientComponent from './ClientComponent';
export default async function ServerComponent() {
const cookieStore = await cookies();
const token = cookieStore.get('token')?.value;
return <ClientComponent token={token} />;
}
原因: cookies()、headers() 和其他仅限服务端的 API 只能在服务端组件中使用。
错误:
export default async function Page() {
const user = await fetchUser();
const posts = await fetchPosts(); // 等待 user 完成
const comments = await fetchComments(); // 等待 posts 完成
return <div>...</div>;
}
正确:
export default async function Page() {
// 并行获取
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
return <div>...</div>;
}
原因: 并行获取显著减少了总加载时间。
错误:
// ClientComponent.tsx
'use client';
import ServerComponent from './ServerComponent'; // 这会使它变成客户端组件!
export default function ClientComponent() {
return <div><ServerComponent /></div>;
}
正确:
// ParentServerComponent.tsx (服务端组件)
import ClientComponent from './ClientComponent';
import ServerComponent from './ServerComponent';
export default function ParentServerComponent() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
);
}
原因: 将服务端组件导入客户端组件会将其转换为客户端组件。应将其作为子元素传递。
客户端组件适用于以下情况:
'use client';
import { useState } from 'react';
export default function ContactForm() {
const [email, setEmail] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// 处理提交
};
return (
<form onSubmit={handleSubmit}>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Submit</button>
</form>
);
}
'use client';
import { useEffect, useState } from 'react';
export default function LiveChat() {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket('wss://chat.example.com');
ws.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};
return () => ws.close();
}, []);
return <div>{messages.map((m, i) => <div key={i}>{m}</div>)}</div>;
}
'use client';
import { useState, useEffect } from 'react';
export default function GeolocationDisplay() {
const [location, setLocation] = useState(null);
useEffect(() => {
navigator.geolocation.getCurrentPosition((pos) => {
setLocation({
lat: pos.coords.latitude,
lng: pos.coords.longitude,
});
});
}, []);
return location ? <div>Lat: {location.lat}, Lng: {location.lng}</div> : null;
}
'use client';
import { useEffect, useState } from 'react';
import confetti from 'canvas-confetti';
export default function CelebrationButton() {
const handleClick = () => {
confetti();
};
return <button onClick={handleClick}>Celebrate!</button>;
}
'use client';
import { createContext, useState } from 'react';
export const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
Need interactivity? (onClick, onChange, etc.)
├─ Yes → Client Component ('use client')
└─ No → Continue...
Need React hooks? (useState, useEffect, etc.)
├─ Yes → Client Component ('use client')
└─ No → Continue...
Need browser APIs? (window, localStorage, etc.)
├─ Yes → Client Component ('use client')
└─ No → Continue...
Need to fetch data?
├─ Yes → Server Component (default)
└─ No → Continue...
Need cookies/headers/searchParams?
├─ Yes → Server Component (default)
└─ No → Server Component (default, unless specific need)
验证组件类型:
// 这有效 = 服务端组件
export default async function MyComponent() { ... }
// 这有效 = 服务端组件
import { cookies } from 'next/headers';
// 这有效 = 客户端组件
'use client';
import { useState } from 'react';
// 这失败 = 错误组合
'use client';
import { cookies } from 'next/headers'; // 错误!
每周安装次数
135
仓库
GitHub 星标数
80
首次出现
2026年1月23日
安全审计
安装于
opencode109
codex107
claude-code107
gemini-cli105
cursor102
github-copilot99
Provide comprehensive guidance for choosing between Server Components and Client Components in Next.js App Router, including cookie/header access, searchParams handling, pathname routing, and React's 'use' API for promise unwrapping.
any TypeCRITICAL RULE: This codebase has @typescript-eslint/no-explicit-any enabled. Using any will cause build failures.
❌ WRONG:
function handleSubmit(e: any) { ... }
const data: any[] = [];
✅ CORRECT:
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }
const data: string[] = [];
// Page props
function Page({ params }: { params: { slug: string } }) { ... }
function Page({ searchParams }: { searchParams: { [key: string]: string | string[] | undefined } }) { ... }
// Form events
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { ... }
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { ... }
// Server actions
async function myAction(formData: FormData) { ... }
Use this skill when:
All components in the App Router are Server Components by default. No directive needed.
// app/components/ProductList.tsx
// This is a Server Component (default)
export default async function ProductList() {
const products = await fetch('https://api.example.com/products');
const data = await products.json();
return (
<ul>
{data.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
When to use Server Components:
Benefits:
Add 'use client' directive at the top of a file to make it a Client Component.
// app/components/Counter.tsx
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
When to use Client Components:
Requirements for Client Components:
'use client' directive at top of fileServer Components are the DEFAULT. DO NOT add 'use client' unless you specifically need client-side features.
✅ CORRECT - Server Component with Navigation:
// app/page.tsx - Server Component (NO 'use client' needed!)
import Link from 'next/link';
import { redirect } from 'next/navigation';
export default async function Page() {
// Server components can be async
const data = await fetchData();
if (!data) {
redirect('/login'); // Server-side redirect
}
return (
<div>
<Link href="/dashboard">Go to Dashboard</Link>
<p>{data.content}</p>
</div>
);
}
❌ WRONG - Adding 'use client' to Server Component:
// app/page.tsx
'use client'; // ❌ WRONG! Don't add this to server components!
export default async function Page() { // ❌ Will fail - async client components not allowed
const data = await fetchData();
return <div>{data.content}</div>;
}
Server Navigation Methods (NO 'use client' needed):
<Link> component from next/linkredirect() function from next/navigationClient Navigation Methods (REQUIRES 'use client'):
useRouter() hook from next/navigationusePathname() hookuseSearchParams() hook (also requires Suspense)Use next/headers to read cookies in Server Components:
// app/dashboard/page.tsx
import { cookies } from 'next/headers';
export default async function Dashboard() {
const cookieStore = await cookies();
const token = cookieStore.get('session-token');
if (!token) {
redirect('/login');
}
const user = await fetchUser(token.value);
return <div>Welcome, {user.name}</div>;
}
Important Notes:
cookies() must be awaited in Next.js 15+// app/api/route.ts or any Server Component
import { headers } from 'next/headers';
export default async function Page() {
const headersList = await headers();
const userAgent = headersList.get('user-agent');
const referer = headersList.get('referer');
return <div>User Agent: {userAgent}</div>;
}
Access URL query parameters directly in Server Components:
// app/search/page.tsx
export default async function SearchPage({
searchParams,
}: {
searchParams: { q?: string; category?: string };
}) {
const query = searchParams.q || '';
const category = searchParams.category || 'all';
const results = await searchProducts(query, category);
return (
<div>
<h1>Search Results for: {query}</h1>
<p>Category: {category}</p>
<ProductList products={results} />
</div>
);
}
Important Notes:
searchParams is only available in page.tsx filessearchParams must be awaitedlayout.tsxuseSearchParams() hook if needed in Client Components⚠️ CRITICAL WARNING - Next.js 15+ searchParams: When extracting parameters in Next.js 15+, you MUST use destructuring to keep the searchParams identifier visible in the same line as the parameter extraction. Do NOT use intermediate variables like params or resolved - this is an anti-pattern that breaks code readability and testing patterns.
Async searchParams (Next.js 15+):
// app/search/page.tsx (Next.js 15+)
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q?: string }>;
}) {
// BEST PRACTICE: Inline access keeps searchParams and parameter together on one line
const q = (await searchParams).q || '';
return <div>Search: {q}</div>;
}
CRITICAL PATTERN REQUIREMENT:
When extracting parameters from searchParams, ALWAYS use inline access to keep searchParams and the parameter name on the SAME LINE:
// ✅ CORRECT: Inline access (REQUIRED PATTERN)
const name = (await searchParams).name || '';
// ✅ ALSO CORRECT: Multiple parameters
const category = (await searchParams).category || 'all';
const sort = (await searchParams).sort || 'asc';
// ❌ WRONG: Using intermediate variable separates searchParams from parameter
const params = await searchParams; // DON'T DO THIS
const name = params.name; // searchParams not visible here
// ❌ WRONG: Destructuring (searchParams and name on same line but missing second 'name')
const { name } = await searchParams; // Not preferred
Why inline access:
searchParams identifier visible on the same line as parameter extractionIn Server Components (page.tsx):
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: { slug: string };
}) {
// params contains route parameters
const post = await getPost(params.slug);
return <article>{post.title}</article>;
}
Async params (Next.js 15+):
// app/blog/[slug]/page.tsx (Next.js 15+)
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.title}</article>;
}
In Client Components:
Use hooks from next/navigation:
// app/components/Breadcrumbs.tsx
'use client';
import { usePathname, useParams, useSearchParams } from 'next/navigation';
export default function Breadcrumbs() {
const pathname = usePathname(); // Current path: /blog/hello-world
const params = useParams(); // Route params: { slug: 'hello-world' }
const searchParams = useSearchParams(); // Query params
return (
<nav>
<span>Current path: {pathname}</span>
<span>Slug: {params.slug}</span>
<span>Search: {searchParams.get('q')}</span>
</nav>
);
}
When usinguseSearchParams() hook, you MUST:
'use client' directive at the top of the fileThis is a Next.js requirement - failing to do both will cause errors.
✅ CORRECT Pattern:
// app/page.tsx or any parent component
import { Suspense } from 'react';
import SearchComponent from './SearchComponent';
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SearchComponent />
</Suspense>
);
}
// app/SearchComponent.tsx
'use client';
import { useSearchParams } from 'next/navigation';
export default function SearchComponent() {
const searchParams = useSearchParams();
const query = searchParams.get('q');
return <div>Search query: {query}</div>;
}
❌ WRONG - Missing 'use client':
// This will fail - useSearchParams requires 'use client'
import { useSearchParams } from 'next/navigation';
export default function SearchComponent() {
const searchParams = useSearchParams(); // ERROR!
return <div>{searchParams.get('q')}</div>;
}
❌ WRONG - Missing Suspense wrapper:
// This will cause issues - useSearchParams requires Suspense
export default function Page() {
return <SearchComponent />; // Missing Suspense wrapper!
}
The React use API allows reading promises and context in both Server and Client Components.
// app/components/UserProfile.tsx
'use client';
import { use } from 'react';
// IMPORTANT: Use specific types, generic types, or 'unknown' - NEVER 'any'
// Option 1: Specific type (best when type is known)
export default function UserProfile({
userPromise
}: {
userPromise: Promise<{ name: string; email: string }>
}) {
// Unwrap the promise
const user = use(userPromise);
return <div>{user.name}</div>;
}
// Option 2: Generic type (for reusable components)
export function GenericDataDisplay<T>({
data
}: {
data: Promise<T>
}) {
const result = use(data);
return <div>{JSON.stringify(result)}</div>;
}
// Option 3: Unknown type (when type truly unknown)
export function UnknownDataDisplay({
data
}: {
data: Promise<unknown>
}) {
const result = use(data);
return <div>{JSON.stringify(result)}</div>;
}
Server Component passing promise:
// app/profile/page.tsx
import UserProfile from './components/UserProfile';
export default function ProfilePage() {
// Create promise but don't await
const userPromise = fetchUser();
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
'use client';
import { use } from 'react';
import { ThemeContext } from './ThemeContext';
export default function ThemedButton() {
const theme = use(ThemeContext);
return <button className={theme.buttonClass}>Click me</button>;
}
// app/products/page.tsx (Server Component)
import ProductGrid from './ProductGrid';
export default async function ProductsPage() {
const products = await fetchProducts();
// Pass data to Client Component
return <ProductGrid products={products} />;
}
// app/products/ProductGrid.tsx (Client Component)
'use client';
import { useState } from 'react';
export default function ProductGrid({
products
}: {
products: Product[]
}) {
const [filter, setFilter] = useState('all');
const filtered = products.filter(p =>
filter === 'all' || p.category === filter
);
return (
<div>
<select onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="electronics">Electronics</option>
</select>
{filtered.map(p => <div key={p.id}>{p.name}</div>)}
</div>
);
}
// app/dashboard/page.tsx
export default async function Dashboard() {
// Fetch in parallel
const [user, stats, notifications] = await Promise.all([
fetchUser(),
fetchStats(),
fetchNotifications(),
]);
return (
<div>
<UserInfo user={user} />
<Stats data={stats} />
<Notifications items={notifications} />
</div>
);
}
// app/page.tsx
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading stats...</div>}>
<Stats />
</Suspense>
<Suspense fallback={<div>Loading feed...</div>}>
<Feed />
</Suspense>
</div>
);
}
async function Stats() {
const data = await fetchStats(); // Slow query
return <div>{data.total}</div>;
}
async function Feed() {
const items = await fetchFeed(); // Fast query
return <ul>{items.map(i => <li key={i.id}>{i.title}</li>)}</ul>;
}
You CAN pass Server Components as children to Client Components:
// app/page.tsx (Server Component)
import ClientWrapper from './ClientWrapper';
import ServerContent from './ServerContent';
export default function Page() {
return (
<ClientWrapper>
{/* Server Component as children */}
<ServerContent />
</ClientWrapper>
);
}
// ClientWrapper.tsx (Client Component)
'use client';
import { useState } from 'react';
export default function ClientWrapper({
children
}: {
children: React.ReactNode
}) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && children}
</div>
);
}
// ServerContent.tsx (Server Component)
export default async function ServerContent() {
const data = await fetchData();
return <div>{data.content}</div>;
}
Wrong:
// app/components/Header.tsx
'use client'; // Unnecessary!
export default function Header() {
return <header><h1>My App</h1></header>;
}
Correct:
// app/components/Header.tsx
// No directive needed - keep it as Server Component
export default function Header() {
return <header><h1>My App</h1></header>;
}
Why: Only use 'use client' when you actually need client-side features. Static components should remain Server Components to reduce bundle size.
Wrong:
'use client';
import { useState, useEffect } from 'react';
export default function Products() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('/api/products')
.then(r => r.json())
.then(setProducts);
}, []);
return <div>{products.map(p => <div key={p.id}>{p.name}</div>)}</div>;
}
Correct:
// Server Component - no 'use client'
export default async function Products() {
const response = await fetch('https://api.example.com/products');
const products = await response.json();
return <div>{products.map(p => <div key={p.id}>{p.name}</div>)}</div>;
}
Why: Server Components can fetch data directly, eliminating loading states and reducing client-side JavaScript.
Wrong:
'use client';
import { cookies } from 'next/headers'; // ERROR!
export default function ClientComponent() {
const cookieStore = cookies(); // This will fail
return <div>...</div>;
}
Correct:
// Server Component
import { cookies } from 'next/headers';
import ClientComponent from './ClientComponent';
export default async function ServerComponent() {
const cookieStore = await cookies();
const token = cookieStore.get('token')?.value;
return <ClientComponent token={token} />;
}
Why: cookies(), headers(), and other server-only APIs can only be used in Server Components.
Wrong:
export default async function Page() {
const user = await fetchUser();
const posts = await fetchPosts(); // Waits for user to finish
const comments = await fetchComments(); // Waits for posts to finish
return <div>...</div>;
}
Correct:
export default async function Page() {
// Fetch in parallel
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
return <div>...</div>;
}
Why: Parallel fetching reduces total load time significantly.
Wrong:
// ClientComponent.tsx
'use client';
import ServerComponent from './ServerComponent'; // This makes it a Client Component!
export default function ClientComponent() {
return <div><ServerComponent /></div>;
}
Correct:
// ParentServerComponent.tsx (Server Component)
import ClientComponent from './ClientComponent';
import ServerComponent from './ServerComponent';
export default function ParentServerComponent() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
);
}
Why: Importing a Server Component into a Client Component converts it to a Client Component. Pass it as children instead.
Client Components are the correct choice for:
'use client';
import { useState } from 'react';
export default function ContactForm() {
const [email, setEmail] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Handle submission
};
return (
<form onSubmit={handleSubmit}>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Submit</button>
</form>
);
}
'use client';
import { useEffect, useState } from 'react';
export default function LiveChat() {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket('wss://chat.example.com');
ws.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};
return () => ws.close();
}, []);
return <div>{messages.map((m, i) => <div key={i}>{m}</div>)}</div>;
}
'use client';
import { useState, useEffect } from 'react';
export default function GeolocationDisplay() {
const [location, setLocation] = useState(null);
useEffect(() => {
navigator.geolocation.getCurrentPosition((pos) => {
setLocation({
lat: pos.coords.latitude,
lng: pos.coords.longitude,
});
});
}, []);
return location ? <div>Lat: {location.lat}, Lng: {location.lng}</div> : null;
}
'use client';
import { useEffect, useState } from 'react';
import confetti from 'canvas-confetti';
export default function CelebrationButton() {
const handleClick = () => {
confetti();
};
return <button onClick={handleClick}>Celebrate!</button>;
}
'use client';
import { createContext, useState } from 'react';
export const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
Need interactivity? (onClick, onChange, etc.)
├─ Yes → Client Component ('use client')
└─ No → Continue...
Need React hooks? (useState, useEffect, etc.)
├─ Yes → Client Component ('use client')
└─ No → Continue...
Need browser APIs? (window, localStorage, etc.)
├─ Yes → Client Component ('use client')
└─ No → Continue...
Need to fetch data?
├─ Yes → Server Component (default)
└─ No → Continue...
Need cookies/headers/searchParams?
├─ Yes → Server Component (default)
└─ No → Server Component (default, unless specific need)
To verify component type:
// This works = Server Component
export default async function MyComponent() { ... }
// This works = Server Component
import { cookies } from 'next/headers';
// This works = Client Component
'use client';
import { useState } from 'react';
// This fails = Wrong combination
'use client';
import { cookies } from 'next/headers'; // ERROR!
Weekly Installs
135
Repository
GitHub Stars
80
First Seen
Jan 23, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode109
codex107
claude-code107
gemini-cli105
cursor102
github-copilot99
Node.js 环境配置指南:多环境管理、类型安全与最佳实践
10,500 周安装
.NET/C# 设计模式代码审查工具 - 自动化分析命令、工厂、仓储等模式实现
8,300 周安装
AI就绪规范创建工具 - 结构化文档生成器,提升AI协作效率
8,300 周安装
智能体评估模式详解:agentic-eval 迭代优化与自我提升框架
8,200 周安装
Vue.js 测试最佳实践指南:Vue 3 单元测试、组件测试与常见问题解决方案
8,400 周安装
Google Workspace CLI Keep 命令:管理 Google Keep 笔记和附件的命令行工具
8,600 周安装
Playwright自动化表单填写教程 - 使用MCP实现高效Web表单自动化测试
8,400 周安装