全栈开发

nextjs表单更新数据不刷新页面

以更新已登录用户的用户信息为案例讲解:

1、请求api根据jwt获取当前用户信息

"use server";
import { fetchWithAuth } from "@/lib/server-fetch";

export async function fetchCurrentUser() {
    try {
        const res = await fetchWithAuth("/user/profile", { method: "GET" });
        const { data } = await res.json();
        return data ?? null; // 直接返回 UserInfo
    } catch (err) {
        console.error("fetchCurrentUser error:", err);
        return null;
    }
}

2、使用React query缓存用户信息

import { fetchCurrentUser } from "@/lib/user";
import { useQuery } from "@tanstack/react-query";

export const useCurrentUser = () =>
  useQuery({
    queryKey: ["currentUser"],
    queryFn: fetchCurrentUser,
    staleTime: 1000 * 60, // 缓存 1 分钟
  });

queryKey: 定义缓存的key

queryFn: 获取数据函数

3、客户端页面使用用户信息

"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Mail, User, FileText } from "lucide-react";
import { UserFormDialog } from "@/app/(protected)/dashboard/settings/user/_components/user-form-dialog";
import { useCurrentUser } from "@/app/(protected)/dashboard/settings/user/services/use-user-queries";
import { MsgLoading } from "@/components/loading-skeleton";

export default function ProfilePage() {
  const { data: user, isLoading } = useCurrentUser();
  if (isLoading) return <MsgLoading />;
  if (!user) return <div>用户未登录</div>;

  const { fullName, email, role } = user;

  return (
    <div className="space-y-6">
      {/* 欢迎区 */}
      <div>
        <h1 className="text-2xl font-bold">{fullName} 的账户详情</h1>
        <p className="text-muted-foreground">您可以查看并修改账户信息</p>
      </div>

      {/* 用户信息卡片 */}
      <Card>
        <CardHeader>
          <CardTitle className="flex items-center gap-2">
            <div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 text-primary">
              <User className="h-5 w-5" />
            </div>
            Account Information
          </CardTitle>
        </CardHeader>
        <CardContent className="space-y-4">
          <div className="flex items-start gap-3">
            <div className="mt-0.5 text-muted-foreground">
              <User className="h-4 w-4" />
            </div>
            <div>
              <p className="text-sm text-muted-foreground">姓名</p>
              <p className="font-medium">{fullName}</p>
            </div>
          </div>

          <div className="flex items-start gap-3">
            <div className="mt-0.5 text-muted-foreground">
              <Mail className="h-4 w-4" />
            </div>
            <div>
              <p className="text-sm text-muted-foreground">邮箱地址</p>
              <p className="font-medium">{email}</p>
            </div>
          </div>

          <div className="flex items-start gap-3">
            <div className="mt-0.5 text-muted-foreground">
              <FileText className="h-4 w-4" />
            </div>
            <div>
              <p className="text-sm text-muted-foreground">角色</p>
              {role ? (
                <Badge variant={role === "ROLE_ADMIN" ? "default" : "secondary"}>
                  {role}
                </Badge>
              ) : (
                <p className="text-muted-foreground"></p>
              )}
            </div>
          </div>
        </CardContent>
      </Card>

      {/* 操作建议区(占位 + 引导) */}
      <Card>
        <CardHeader>
          <CardTitle>Need to make changes?</CardTitle>
        </CardHeader>
        <CardContent>
          <p className="text-sm text-muted-foreground mb-3">
            Update your profile, change password, or manage notification preferences.
          </p>
          <UserFormDialog currentUser={user} />
        </CardContent>
      </Card>
    </div>
  );
}

4、编辑用户信息模态框组件编写

4.1、定义表单验证规则

import z from "zod";

// 1. 定义表单字段的验证 schema
const userSchema = z.object({
  fullName: z.string().min(1, "姓名不能为空!").max(255),
  email: z.string().email("输入邮箱格式错误!"),
});

// 2. 推导 TypeScript 类型
type UserSchema = z.infer<typeof userSchema>;

// 3. 默认值(从当前用户状态来)
const userDefaultValues: UserSchema = {
  fullName: "",   // 实际使用时会被当前用户数据覆盖
  email: "",
};

export {userDefaultValues, userSchema, type UserSchema};

4.2 组件编写

"use client";

import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import {
    Dialog,
    DialogContent,
    DialogHeader,
    DialogTitle,
    DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { ControlledInput } from "@/components/controlled/controlled-input";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { userSchema, UserSchema } from "@/app/(protected)/dashboard/settings/user/_types/user-schema";
import { zodResolver } from "@hookform/resolvers/zod";

interface UserFormDialogProps {
    currentUser: {
        id: number;
        fullName: string;
        email: string;
    };
}

export function UserFormDialog({ currentUser }: UserFormDialogProps) {
    const [open, setOpen] = useState(false);
    const queryClient = useQueryClient();

    const form = useForm<UserSchema>({
        defaultValues: {
            fullName: currentUser.fullName,
            email: currentUser.email,
        },
        resolver: zodResolver(userSchema),
    });

    const mutation = useMutation({
        mutationFn: (data: UserSchema) =>
            fetch("/api/user/profile/update", {
                method: "PUT",
                body: JSON.stringify({ id: currentUser.id, ...data })
            }).then((res) => res.json()),
        onSuccess: (result) => {
            if (result.code === 200 && result.data) {
                // 同时刷新当前用户 query,让其他组件也更新
                queryClient.invalidateQueries({ queryKey: ["currentUser"] });
                toast.success("修改成功");
                setOpen(false);
            } else {
                toast.error(result.message || "修改失败");
            }
        },
        onError: () => {
            toast.error("服务异常,请稍后再试");
        },
    });

    const onSubmit: SubmitHandler<UserSchema> = (data) => {
        mutation.mutate(data);
    };

    return (
        <Dialog open={open} onOpenChange={setOpen}>
            <DialogTrigger asChild>
                <Button>编辑信息</Button>
            </DialogTrigger>

            <DialogContent>
                <DialogHeader>
                    <DialogTitle>编辑用户信息</DialogTitle>
                </DialogHeader>

                <FormProvider {...form}>
                    <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
                        <ControlledInput name="fullName" label="姓名" placeholder="请输入姓名" />
                        <ControlledInput name="email" label="邮箱" placeholder="请输入邮箱" type="email" />
                        <Button type="submit">确认修改</Button>
                    </form>
                </FormProvider>
            </DialogContent>
        </Dialog>
    );
}

说明:

  • useFrom定义表单传值占位以及验证
    • zodResolver 验证表单数据
  • useState控制模态框显式和隐藏
  • useMuation定义异步操作成功失败处理
    • fetch("/api开头") 使用了自定义的api代理访问后端
    • fecth("/api").then((res) => res.json()) 返回接口数据
  • queryClient.invalidateQueries({ queryKey: ["currentUser"] });
    • 指定缓存key数据无效,使用该数据的地方重新请求数据,重新渲染到页面
  • ControlledInput自定义input,集成验证错误信息显示
"use client";
import { ComponentProps } from "react"
import { Controller, FieldValues, Path, useFormContext } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Field, FieldLabel, FieldError } from "@/components/ui/field";

type ControlledInputProps<T extends FieldValues> = {
    name: Path<T>;
    label?: string;
} & ComponentProps<"input">;

const ControlledInput = <T extends FieldValues>({
    className,
    type,
    name,
    label,
    ...props
}: ControlledInputProps<T>) => {
    const { control } = useFormContext<T>();
    return (
        <Controller
            name={name}
            control={control}
            render={({ field, fieldState: { error } }) => (
                <Field>
                    {!!label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
                    <Input
                        id={name}
                        type={type}
                        className={className}
                        {...field}
                        {...props}
                    />
                    {error && <FieldError>{error.message}</FieldError>}
                </Field>
            )}
        />
    )
};

export { ControlledInput };