前几天了解了Nextjs做的prefetch的一些原理,然后看到了一个很好玩的hook,也就是乐观更新这个概念(我觉得是挺有用的(bushi))
我们先看一下官方给的解释
useOptimistic 是一个 React Hook,它允许你在进行异步操作时显示不同 state。它接受 state 作为参数,并返回该 state 的副本,在异步操作(如网络请求)期间可以不同。你需要提供一个函数,该函数接受当前 state 和操作的输入,并返回在操作挂起期间要使用的乐观状态。
这个状态被称为“乐观”状态是因为通常用于立即向用户呈现执行操作的结果,即使实际上操作需要一些时间来完成。
tsxconst [optimisticState, addOptimistic] = useOptimistic(state, updateFn);
也就是说,当页面进行更新(如网络请求,异步操作等需要消耗一定时间的操作)时,我们可以假设它更新是成功的(你得自信,自信使你的页面快乐),并且立刻向页面呈现状态
tsximport { useOptimistic } from 'react';
function AppContainer() {
const [optimisticState, addOptimistic] = useOptimistic(
state,
// 更新函数
(currentState, optimisticValue) => {
// 使用乐观值
// 合并并返回新 state
}
);
}
其中
(currentState, optimisticValue) => newState,它会在你调用 addOptimistic() 时运行,把「真实数据」和「乐观数据」合并,得到一个临时的新 state。失败是小概率事件,但成功是大概率事件。 与其等成功的返回,不如直接假设成功。
相信聪明的你已经想到了,其实我们的使用场景也很简单,我目前认为最合适,最适配的就是类似于评论区之类的东西,我们来写一个实现一键三连的小组件实战一下
tsx"use client";
import { useOptimistic } from "react";
import { Heart, Coins, Bookmark } from "lucide-react";
import { likeVideo, coinVideo, favoriteVideo } from "@/app/actions";
type Stats = {
likes: number;
coins: number;
favorites: number;
};
type Props = {
initial: Stats; // Initial counts from server
videoId: string; // ID of the current video
};
/**
* @example
* ```tsx
* <BilibiliActions
* videoId="abc123"
* initial={{ likes: 120, coins: 50, favorites: 33 }}
* />
* ```
*/
export default function BilibiliActions({ initial, videoId }: Props) {
/**
* useOptimistic hook
*
* @param state - The current stats (likes, coins, favorites)
* @param action - The action to apply (which field to increment)
* @returns Updated stats with optimistic increment applied
*/
const [optimisticStats, updateOptimistic] = useOptimistic(
initial,
(state: Stats, action: { type: keyof Stats }) => {
return { ...state, [action.type]: state[action.type] + 1 };
}
);
/**
* Handles user interaction and sends server request.
* First updates UI optimistically, then calls the corresponding server action.
*
* @param type - Which stat to increment ("likes" | "coins" | "favorites")
*/
async function handleAction(type: keyof Stats) {
updateOptimistic({ type });
switch (type) {
case "likes":
await likeVideo(videoId);
break;
case "coins":
await coinVideo(videoId);
break;
case "favorites":
await favoriteVideo(videoId);
break;
}
}
return (
<div className="flex gap-6 items-center">
{/* Like button */}
<button
onClick={() => handleAction("likes")}
className="flex items-center gap-1 text-gray-600 hover:text-pink-500 transition"
>
<Heart className="w-5 h-5" />
<span className="text-sm">{optimisticStats.likes}</span>
</button>
{/* Coin button */}
<button
onClick={() => handleAction("coins")}
className="flex items-center gap-1 text-gray-600 hover:text-yellow-500 transition"
>
<Coins className="w-5 h-5" />
<span className="text-sm">{optimisticStats.coins}</span>
</button>
{/* Favorite button */}
<button
onClick={() => handleAction("favorites")}
className="flex items-center gap-1 text-gray-600 hover:text-blue-500 transition"
>
<Bookmark className="w-5 h-5" />
<span className="text-sm">{optimisticStats.favorites}</span>
</button>
</div>
);
}
你也发现了,乐观更新的好处还有之一:也就是后端不再返回数据,只需要一个成功or失败就可以了
首先我们要知道transition是干什么的:
transiton将会标记一个state为异步非阻塞状态(也就是说这一次的更新并不会阻塞渲染),比如你的某个组件可能会进行高频更新(假如用户反复点击点赞按钮,状态并不会第一时间更新,而是等这一阶段的transition结束之后再更新state)
tsx"use client";
import { useOptimistic, useTransition } from "react";
import { startTransition as s } from "react"; // 也可用 useTransition
import { likeVideo } from "@/app/actions";
export function LikeWithOptimistic({ initial, videoId }: { initial: number; videoId: string }) {
const [likes, setLikes] = useState(initial);
const [optimisticLikes, addOptimistic] = useOptimistic(likes, (s, step: number) => s + step);
const [isPending, startTransition] = useTransition();
async function onLike() {
addOptimistic(1); // 立即+1
// 低优先级刷新真实数据(例如从服务端返回最新计数)
startTransition(async () => {
const next = await likeVideo(videoId); // server action
setLikes(next);
});
}
return (
<button onClick={onLike} className="btn">
👍 {optimisticLikes} {isPending && <small className="ml-2 text-gray-500">同步中</small>}
</button>
);
}
知道原理了,我们直接手搓一个! 其实我们仔细思考一下无非就是
(还没实现 主包困了明天再写)


本文作者:MapleCity
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!