如何在Next.js 13 App Router中用'use client'组件正确渲染服务器初始数据?
在Next.js 13 App Router中实现SSR初始数据 + 客户端动态请求的规范方案
核心思路
利用App Router的服务端组件传递初始数据给客户端组件,结合客户端状态管理与API调用,既保证SSR渲染初始内容(禁用JS时仍有完整HTML),又支持客户端无刷新交互。你的临时方案方向正确,以下是更简洁规范的优化实现:
具体实现步骤
1. 服务端页面组件(page.tsx)
负责服务端获取初始数据,作为props传递给客户端组件,确保页面SSR渲染。
// app/search/page.tsx import ProductSearch from './ProductSearch'; // 服务端组件:无需标注'use client' export default async function SearchPage({ searchParams, }: { searchParams: { q?: string }; }) { // 服务端请求初始数据:首次访问或URL带搜索参数时执行 const initialProducts = await fetchProducts(searchParams.q || ''); return ( <div> <h1>产品搜索</h1> {/* 传递初始数据与初始搜索词给客户端组件 */} <ProductSearch initialProducts={initialProducts} initialQuery={searchParams.q || ''} /> </div> ); } // 服务端API请求函数(可抽离至单独文件复用) async function fetchProducts(query: string) { const res = await fetch( `https://api.your-domain.com/products?q=${encodeURIComponent(query)}`, { next: { revalidate: 60 } } // 可选:设置数据缓存有效期,优化性能 ); if (!res.ok) throw new Error('获取产品列表失败'); return res.json(); }
2. 客户端交互组件(ProductSearch.tsx)
标记'use client',接收服务端传递的初始数据,管理客户端状态,处理后续无刷新搜索请求。
// app/search/ProductSearch.tsx 'use client'; import { useState } from 'react'; type Product = { id: string; name: string; price: number; }; type Props = { initialProducts: Product[]; initialQuery: string; }; export default function ProductSearch({ initialProducts, initialQuery }: Props) { // 用服务端初始数据初始化客户端状态 const [products, setProducts] = useState<Product[]>(initialProducts); const [query, setQuery] = useState(initialQuery); const [isLoading, setIsLoading] = useState(false); // 客户端搜索逻辑:无刷新请求API const handleSearch = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); try { // 直接调用外部API或通过Next.js API路由代理 const res = await fetch(`/api/products?q=${encodeURIComponent(query)}`); if (!res.ok) throw new Error('搜索失败'); const data = await res.json(); setProducts(data); // 可选:更新URL搜索参数,保证页面可分享、刷新后状态保留 window.history.pushState({}, '', `/search?q=${encodeURIComponent(query)}`); } catch (err) { console.error(err); } finally { setIsLoading(false); } }; return ( <div> <form onSubmit={handleSearch}> <input type="text" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="输入产品名称搜索" /> <button type="submit" disabled={isLoading}> {isLoading ? '搜索中...' : '搜索'} </button> </form> <div className="product-list"> {products.map((product) => ( <div key={product.id} className="product-item"> <h3>{product.name}</h3> <p>¥{product.price}</p> </div> ))} </div> </div> ); }
3. 可选:客户端API路由代理(app/api/products/route.ts)
如果需要统一处理请求逻辑(如添加鉴权、缓存),可通过Next.js API路由做一层代理:
// app/api/products/route.ts import { NextResponse } from 'next/server'; export async function GET(request: Request) { const { searchParams } = new URL(request.url); const q = searchParams.get('q') || ''; const res = await fetch(`https://api.your-domain.com/products?q=${encodeURIComponent(q)}`); const data = await res.json(); return NextResponse.json(data); }
方案优势
- SSR兼容:首次访问时服务端渲染完整HTML,禁用JS也能看到初始产品列表
- 无刷新交互:启用JS后,后续搜索通过客户端fetch请求,无需刷新页面
- 状态无缝衔接:初始数据直接从服务端传递,避免客户端二次请求与 hydration 不匹配
- 职责清晰:服务端负责数据获取,客户端负责交互逻辑,代码可维护性强
常见误区规避
- 不要在客户端组件中直接用
fetch获取初始数据:会导致客户端二次请求,且禁用JS时无内容 - 确保初始数据类型与客户端状态一致,避免 hydration 错误
- 若需支持URL状态同步,记得用
window.history.pushState更新URL,保证页面可分享、刷新后状态保留
内容的提问来源于stack exchange,提问作者WhiteJackal
相关产品推荐
相关产品推荐

