Next.js中useOptimistic乐观UI更新失效问题求助
乐观UI更新后重置问题:Next.js 14 PostItem组件点赞/收藏异常
我正在开发Web应用的PostItem组件,技术栈为Next.js 14、TypeScript、TailwindCSS和Prisma。实现用户点赞或收藏帖子的乐观UI更新功能时遇到异常:点击点赞后,UI会立即更新点赞数(增加/减少),但服务器请求完成后,UI会重置为操作前的状态,尽管后端并未返回错误。
相关代码
PostItem.tsx
"use client"; import { Bookmark, Heart, MessageCircle } from "lucide-react"; import { UserAvatar } from "./UserAvatar"; import Link from "next/link"; import Image from "next/image"; import { AspectRatio } from "../shadcn/ui/aspect-ratio"; import { Skeleton } from "../shadcn/ui/skeleton"; import { formatDateToString } from "@/lib/utils"; import { deletePost, patchPostLike, patchPostSave, } from "@/lib/actions/post.actions"; import { useState, useOptimistic } from "react"; import LoginModal from "./LoginModal"; interface PostItemProps { image: string; likes: string[]; saves: string[]; comments: number; id: string; author: { name: string; image: string; id: string }; userId?: string; createdAt: Date; } function PostItem({ image, likes, saves, comments, id, userId, author, createdAt, }: PostItemProps) { const [serverLikes, setServerLikes] = useState(likes); const [serverSaves, setServerSaves] = useState(saves); const [serverLiked, setServerLiked] = useState( serverLikes.includes(userId || "") ); const [serverSaved, setServerSaved] = useState( serverSaves.includes(userId || "") ); const [optimisticLikes, setOptimisticLikes] = useOptimistic(serverLikes); const [optimisticLiked, setOptimisticLiked] = useOptimistic(serverLiked); const [optimisticSaved, setOptimisticSaved] = useOptimistic(serverSaved); const userURL = `/user/${author.name}`; const postURL = `/post/${id}`; const formattedDate = formatDateToString(createdAt); const handleLikeClick = async () => { if (!userId) return; try { const updatedLikes = serverLiked ? serverLikes.filter((like: string) => like !== userId) : [...serverLikes, userId]; setOptimisticLikes(updatedLikes); setOptimisticLiked(updatedLikes.includes(userId)); const res = await patchPostLike(id, userId, serverLikes); if (res.error != undefined) { return; } setServerLikes(res.success); setServerLiked(res.success.includes(userId)); } catch (error: any) { console.error("Failed to update like:", error.message); } }; const handleSaveClick = async () => { if (!userId) return; try { const updatedSaves = serverSaved ? serverSaves.filter((save: string) => save !== userId) : [...serverSaves, userId]; setOptimisticSaved(updatedSaves.includes(userId)); const res = await patchPostSave(id, userId, serverSaves); if (res.error != undefined) { return; } setServerSaves(res.success); setServerSaved(res.success.includes(userId)); } catch (error: any) { console.error("Failed to update save:", error.message); } }; return ( <div className="p-3 pl-0"> <div className="flex items-start"> <Link href={userURL} className="flex items-center gap-x-3"> <UserAvatar user={author} /> <div> <p className="text-sm">{author.name}</p> <p className="text-xs text-muted-foreground"> {formattedDate} </p> </div> </Link> </div> <Link href={postURL}> <AspectRatio ratio={2 / 2.75} className="mt-3 bg-muted"> <Image src={image} alt={id} fill className="object-cover rounded-lg" /> </AspectRatio> </Link> <div className="flex items-center justify-between gap-3 mt-3"> <div className="flex items-center gap-x-4"> <div className="flex items-center"> {userId !== undefined ? ( <Heart className={`w-5 h-5 hover:cursor-pointer hover:opacity-70 ${ optimisticLiked ? "text-destructive fill-destructive" : "" }`} onClick={handleLikeClick} /> ) : ( <LoginModal> <Heart className="w-5 h-5 hover:cursor-pointer hover:opacity-70" /> </LoginModal> )} <p className="ml-1 text-sm text-muted-foreground"> {optimisticLikes.length} </p> </div> <div className="flex items-center"> <MessageCircle className="w-5 h-5" /> <p className="ml-1 text-sm text-muted-foreground hover:opacity-70"> {comments} </p> </div> </div> {userId !== undefined ? ( <Bookmark className={`w-5 h-5 hover:cursor-pointer hover:opacity-70 ${ optimisticSaved ? "text-yellow-400 fill-yellow-400" : "" }`} onClick={handleSaveClick} /> ) : ( <LoginModal> <Bookmark className="w-5 h-5 hover:cursor-pointer hover:opacity-70" /> </LoginModal> )} </div> </div> ); } PostItem.Skeleton = function PostItemSkeleton() { return ( <div> <div className="flex items-start"> <div className="flex items-center gap-x-3"> <Skeleton className="w-12 h-12 rounded-full" /> <div className="space-y-2"> <Skeleton className="h-4 w-[80px]" /> <Skeleton className="h-4 w-[125px]" /> </div> </div> </div> <div> <AspectRatio ratio={2 / 2.75} className="mt-3"> <Skeleton className="w-full h-full rounded-lg" /> </AspectRatio> </div> </div> ); }; export default PostItem;
post.actions.ts
"use server"; export async function patchPostLike( postId: string, userId: string, likedBy: string[] ) { const user = await getCurrentUser(); if (!user) return { error: "Unauthorized" }; try { const isLiked = likedBy.some((like) => like === userId); const post = await client.post.update({ where: { id: postId, }, data: { likedBy: { [isLiked ? "deleteMany" : "create"]: { userId: userId, }, }, }, include: { likedBy: true, }, }); const likes = post.likedBy.map((i) => { return i.userId; }); return { success: likes }; } catch (error: any) { return { error: `Failed to add like to post: ${error.message}` }; } } export async function patchPostSave( postId: string, userId: string, savedBy: string[] ) { const user = await getCurrentUser(); if (!user) return { error: "Unauthorized" }; try { const isSaved = savedBy.some((save) => save === userId); const post = await client.post.update({ where: { id: postId }, data: { savedBy: { [isSaved ? "deleteMany" : "create"]: { userId: userId, }, }, }, include: { savedBy: true }, }); const saves = post.savedBy.map((i) => { return i.userId; }); return { success: saves }; } catch (error: any) { return { error: `Failed to save post: ${error.message}` }; } }
已尝试方案
移除serverLikes状态,将useOptimistic钩子的fallback值设为props传入的likes,但组件无法在请求完成后更新点赞状态,问题仍未解决。
内容的提问来源于stack exchange,提问作者Spoeky
相关产品推荐
相关产品推荐

