You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 13:42:36