NextJS 乐观 UI 更新与 useOptimistic 不起作用

NextJS optimistic UI updates with useOptimistic not working

提问人:Spoeky 提问时间:11/18/2023 更新时间:11/18/2023 访问量:64

问:

我正在尝试为我的 Web 应用程序创建一个 PostItem 组件。我使用 NextJs 14、TypeScript、TailwindCSS 和 Prisma。 3 天多来,我一直在尝试在用户喜欢或保存帖子时实现乐观的 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}` };
    }
}

我尝试了各种方法,例如没有状态,而是将 useOptimistic 钩子的“后备”值作为通过 props 传入的值。但是,这不起作用,因为在从组件内部发出请求后,点赞将无法更新。serverLikeslikes

打字稿 next.js next.js13 optimistic-ui 服务器操作

评论

0赞 hulin 11/18/2023
有什么可行的演示可以尝试吗?
0赞 Spoeky 11/18/2023
可悲的是,@hulin没有

答: 暂无答案