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

Next.js客户端组件调用Server Action报错:next/headers仅支持服务端组件

客户端组件调用Server Action时的next/headers导入错误

错误提示:
You're importing a component that needs next/headers. That only works in a Server Component but one of its parents is marked with "use client", so it's a Client Component.

相关代码

actions.ts(Server Action定义)

import { lucia } from "@/auth";
import prisma from "@/lib/prisma";
import { signUpSchema, SignUpValues } from "@/lib/validation";
import { hash } from "@node-rs/argon2";
import { generateIdFromEntropySize } from "lucia";
import { isRedirectError } from "next/dist/client/components/redirect";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

export async function signUp(
  credentials: SignUpValues
): Promise<{ error: string }> {
  try {
    const { username, email, password } = signUpSchema.parse(credentials);

    const passwordHash = await hash(password, {
        // recommended minimum parameters
        memoryCost: 19456,
        timeCost: 2,
        outputLen: 32,
        parallelism: 1
    });
    const userId = generateIdFromEntropySize(10); // 16 characters long

    const existingUsername = await prisma.user.findFirst({
        where:{
            username:{
                equals: username,
                mode: "insensitive"
            }
        }
    });

    if (existingUsername){
        return {error: "Username already taken"}
    }

    const existingEmail = await prisma.user.findFirst({
        where:{
            username:{
                equals: email,
                mode: "insensitive"
            }
        }
    });

    if (existingEmail){
        return {error: "Email already taken"}
    }

    await prisma.user.create({
        data:{
            id: userId,
            username: username,
            displayName: username,
            email: email,
            passwordHash: passwordHash
        }
    })

    const session = await lucia.createSession(userId, {});
    const sessionCookie = lucia.createSessionCookie(session.id);
    cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
    return redirect("/");


  } catch (error) {
    if (isRedirectError(error)) throw error;
    console.log(error);
    return{
        error: "Something went wrong. Please try again"
    }
  }
}

SignupForm.tsx(客户端组件)

/* eslint-disable @typescript-eslint/no-unused-vars */
"use client";

// import LoadingButton from "@/components/LoadingButton";
// import { PasswordInput } from "@/components/PasswordInput";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { signUpSchema, SignUpValues } from "@/lib/validation";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState, useTransition } from "react";
import { useForm } from "react-hook-form";
import { signUp } from "./actions";
import { Button } from "@/components/ui/button";

export default function SignUpForm() {
  const [error, setError] = useState<string>();

  const [isPending, startTransition] = useTransition();

  const form = useForm<SignUpValues>({
    resolver: zodResolver(signUpSchema),
    defaultValues: {
      email: "",
      username: "",
      password: "",
    },
  });

  async function onSubmit(values: SignUpValues) {
    setError(undefined);
    startTransition(async () => {
      const { error } = await signUp(values);
      if (error) setError(error);
    });
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-3">
        {error && <p className="text-center text-destructive">{error}</p>}
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Username</FormLabel>
              <FormControl>
                <Input placeholder="Username" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input placeholder="Email" type="email" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="password"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Password</FormLabel>
              <FormControl>
                <Input placeholder="Password" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit" className="w-full">
          Create account
        </Button>
      </form>
    </Form>
  );
  
}

解决方法

  1. 标记Server Action文件:在actions.ts最顶部添加'use server'指令,明确这是服务端执行的代码,避免客户端误导入服务端专属API:

    'use server';
    // 后续的导入和代码保持不变
    
  2. 修正客户端调用逻辑:客户端组件不能直接执行Server Action函数,需通过表单action属性绑定,结合react-hook-form调整提交逻辑:
    修改SignupForm.tsx中的提交处理:

    // 替换原onSubmit函数
    async function handleFormSubmit(formData: FormData) {
      setError(undefined);
      const values = Object.fromEntries(formData) as SignUpValues;
      startTransition(async () => {
        const result = await signUp(values);
        if (result.error) setError(result.error);
      });
    }
    
    // 同步修改form标签的onSubmit属性
    <form onSubmit={form.handleSubmit(handleFormSubmit)} className="space-y-3">
    
  3. 修复业务逻辑bug:signUp函数中检查邮箱重复的条件错误,将where中的username改为email:

    const existingEmail = await prisma.user.findFirst({
        where:{
            email:{
                equals: email,
                mode: "insensitive"
            }
        }
    });
    

内容的提问来源于stack exchange,提问作者darel noutsa

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 11:45:01