文章
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 };