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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 04:14:51