Tao
Tao

Next.js 中的 cannot use both 'use client' and export function 'generateStaticParams()' 报错解决

在Next.js(版本13及更高版本)中的引入了服务器组件、客户端组件(use client指令),这些术语是在react18.0版本提出来的。如果对这两个组件的不熟悉吧,就很容易出现这个的错误“cannot use both ‘use client’ and export function generateStaticParams()”。本文将剖析这个冲突发生的原因,更重要的是,将告诉你如何正确的代码组织方式,避免出现这样的问题。

React服务器组件(React Server Components):通过在服务器端进行渲染的组件,这些界面不仅能在服务器端进行渲染,还支持服务器端缓存。

在Next.js框架中,渲染任务会依据路由片段(route segments)进行进一步的细分,旨在启用流式传输 (streaming) 和部分渲染 (partial rendering)功能。基于此,Next.js提供了三种不同的服务器端渲染策略:

  • 静态渲染 (Static Rendering)
  • 动态渲染 (Dynamic Rendering)
  • 流式渲染 (Streaming)

客户端组件 (React Client Components):通过浏览器客户端进行交互运行的组件,这些界面会在服务器端进行预渲染,并且能够利用客户端 JavaScript 在浏览器中运行。” 通常使用use client 指令表示组件需要在客户端运行。

当你在文件顶部放置 "use client" 时,你是在告诉 Next.js:“此文件中的所有内容及其导入的任何组件都是客户端组件。”

generateStaticParams 函数是你进行动态路由(例如 app/blog/[slug]/page.tsx)通过SSG技术生成静态html,这种行为对SEO至关重要。

  • generateStaticParams 在服务器上构建时执行。
  • 它告诉 Next.js 哪些路径应该被预渲染成静态 HTML。例如,它可能会获取你所有的博客文章 slug,并返回一个类似 [{ slug: 'my-first-post' }, { slug: 'another-awesome-article' }] 的数组。
  • 缺点:它无法访问浏览器 API 或客户端 React 特性。

当你在文件顶部放置 "use client" 时,你是在告诉 Next.js:“此文件中的所有内容及其导入的任何组件都是客户端组件。”

  • 可以使用像 useStateuseEffectuseContext 这样的 React Hooks,处理浏览器事件(例如 onClick),并与浏览器 API 交互。
  • 虽然客户端组件会在服务器上为初始HTML进行预渲染,但它们的JavaScript包会发送到客户端以hydrate页面,它们在浏览器中执行

那么,为什么会报错呢?这归根结底在于它们根本不同的执行环境和时机:

  • generateStaticParams() 只需要在构建过程中于服务器上运行,这远早于任何客户端 JavaScript 发挥作用之前。它负责规划你静态站点的结构
  • "use client" 指定了那些需要浏览器环境才能运行的代码,包括其交互功能。

你不能让单个文件既充当用于静态路径的构建时、仅限服务器的清单,又充当客户端浏览器代码的入口点。Next.js 需要一个明确的区分。如果一个页面文件(例如 page.tsx)导出了 generateStaticParams,那么它本质上在其根部就是一个服务器组件。在该文件中同时声明 "use client" 会造成一种不可能的情况。

以下是应遵循的模式:

  1. 保持你的页面作为服务器组件: 导出 generateStaticParams 的文件(例如 app/blog/[slug]/page.tsx)应保持为服务器组件。不要使用React Hooks相关的API,保证页面交互都在客户端进行。

  2. 创建一个专用的客户端组件: 如果你需要该静态生成页面内的客户端交互性(例如,按钮、有状态的 UI、动画),请创建一个新的组件文件。

    • "use client" 放置在这个新组件文件的顶部。
    • 在此实现你所有的交互逻辑。
  3. 将客户端组件导入到你的服务器页面组件中: 你的服务器组件(即页面)随后可以导入并渲染这个新的客户端组件,并将任何必要的数据作为 props 传递给它。

假设有一个动态博客文章页面,它需要 generateStaticParams,并且还有一个交互式的“点赞”按钮。

1. app/blog/[slug]/page.tsx (服务器组件) (代码示例保持英文,但注释会翻译)

typescript

// 这里没有 "use client"!

import BlogLayout from '../../../components/BlogLayout'; // 我们新的客户端组件
import { getPostBySlug, getAllPostSlugs } from '../../../lib/posts'; // 你的数据获取函数

// 1. 为每篇博客文章生成静态路径
export async function generateStaticParams() {
  const slugs = await getAllPostSlugs(); // 获取所有的 [{ slug: '...' }]
  return slugs.map((item: { slug: string }) => ({ // 为 item 添加了类型
    slug: item.slug,
  }));
}

// 2. 获取特定文章的数据
async function getPostData(slug: string) {
  const post = await getPostBySlug(slug);
  return post;
}

export default async function BlogPostPage({ params }: { params: { slug: string } }) {
  const postData = await getPostData(params.slug);

  if (!postData) {
    // 你可能想从 'next/navigation' 返回 notFound()
    return <div>文章未找到!</div>;
  }

  // 3. 将服务器获取的数据传递给客户端组件
  return (
    <main>
      <h1>{postData.title}</h1> {/* 在服务器上渲染 */}
      <BlogLayout content={postData.content} initialLikes={postData.likes} postId={postData.id} />
    </main>
  );
}

2. components/BlogLayout.tsx (客户端组件) (代码示例保持英文,但注释会翻译)

typescript

"use client"; // 这现在是一个客户端组件

import { useState, useEffect } from 'react';

interface BlogLayoutProps {
  content: string;
  initialLikes: number;
  postId: string;
}

export default function BlogLayout({ content, initialLikes, postId }: BlogLayoutProps) {
  const [likes, setLikes] = useState(initialLikes);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    // 如果需要,你可以在这里获取额外的客户端数据
    console.log(`文章 ${postId} 的交互内容已加载`);
  }, [postId]);

  const handleLike = async () => {
    setIsLoading(true);
    // 模拟 API 调用来更新点赞数
    await new Promise(resolve => setTimeout(resolve, 500));
    setLikes(prevLikes => prevLikes + 1);
    setIsLoading(false);
    // 在真实应用中,你还需要将此更新发送到后端
    console.log(`文章 ${postId} 已点赞!新计数: ${likes + 1}`);
  };

  return (
    <div>
      <div dangerouslySetInnerHTML={{ __html: content }} />
      <button onClick={handleLike} disabled={isLoading}>
        {isLoading ? '点赞中...' : `👍 点赞 (${likes})`}
      </button>
      {/* 其他交互式 UI 元素可以放在这里 */}
    </div>
  );
}
  • 性能: 你仍然可以获得静态生成页面带来的极快加载时间和SEO优势。
  • 交互性: 你可以在需要的地方无缝集成动态、交互式元素。
  • 清晰分离: 你的代码变得更有条理,服务器端关注点(为 SSG 获取数据、路由)和客户端关注点(UI 交互、状态管理)被清晰地分离开来。
  • 减少客户端包大小: 只有你的客户端组件的 JavaScript 会被发送到浏览器,从而使你的初始页面加载保持精简。

"cannot use both 'use client' and export function 'generateStaticParams()'" 这个错误实际上是Next.js编译器在提醒你的代码写的有问题。通过理解服务器组件和客户端组件并合理的使用他们,你可以构建出既快速又具有交互性的复杂应用程序。