Optimistic Mutation Frame
樂觀更新容器:操作後立即插入半透明幽靈項目,伺服器確認時凝固實體化,失敗時顫抖回退並將錯誤移交 toast。
載入預覽⋯
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
2
8
4000
<OptimisticMutationFrame />
安裝
npx shadcn@latest add https://webberui.com/r/optimistic-mutation-frame.json或在 components.json 設定 registries 後,改用 @webberui/optimistic-mutation-frame 安裝。
安裝依賴後,從 registry JSON(/r/optimistic-mutation-frame.json 的 files[0].content)複製 optimistic-mutation-frame.tsx 原始碼到你的 components/ui/ 目錄:
npm install motion clsx tailwind-merge lucide-react使用
OptimisticFrame 包住任意列表或卡片,透過 ref 取得指令式操作介面。呼叫 add 會立即插入一個半透明的幽靈項目,並等待你傳入的非同步 task:成功則凝固為實體、失敗則顫抖回退並跳出 toast。
import * as React from "react";
import {
OptimisticFrame,
type OptimisticFrameHandle,
} from "@/components/ui/optimistic-mutation-frame";
interface Todo {
id: string;
text: string;
}
export function Example() {
const frame = React.useRef<OptimisticFrameHandle<Todo>>(null);
function handleAdd(todo: Todo) {
frame.current?.add(todo, async () => {
const res = await fetch("/api/todos", {
method: "POST",
body: JSON.stringify(todo),
});
if (!res.ok) throw new Error("儲存失敗");
return (await res.json()) as Todo; // 以伺服器回傳覆蓋樂觀資料
});
}
return (
<OptimisticFrame<Todo>
ref={frame}
defaultItems={seed}
getKey={(todo) => todo.id}
>
{(entry) => (
<div data-pending={entry.pending}>{entry.data.text}</div>
)}
</OptimisticFrame>
);
}task 回傳的值(若有)會覆蓋樂觀插入的資料,讓你把伺服器產生的 id、時間戳等真實欄位補回項目。丟出例外即代表失敗,觸發回退。
Props
OptimisticFrame
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
getKey | (item: T) => string | — | 從資料取出穩定 key |
children | (entry: OptimisticEntry<T>) => ReactNode | — | render-prop,依 entry.status / entry.pending 客製樣式 |
defaultItems | T[] | [] | 初始的實體項目(非受控,只讀取一次) |
layout | "list" | "grid" | "list" | 佈局方向 |
columns | number | 2 | grid 佈局時的欄數 |
gap | number | 8 | 項目間距(px) |
toast | boolean | true | 是否顯示內建錯誤 toast |
toastDuration | number | 4000 | toast 自動消失時間(毫秒) |
onError | (error, ctx) => void | — | 失敗時觸發,可串接外部 toast/回報 |
itemClassName | string | — | 附加到每個項目外框的樣式 |
ref | Ref<OptimisticFrameHandle<T>> | — | 取得指令式操作介面 |
OptimisticFrameHandle(透過 ref)
| 方法 | 簽名 | 說明 |
|---|---|---|
add | (data, task, options?) => Promise<boolean> | 樂觀新增;task 成功凝固、失敗回退,回傳是否確認成功 |
remove | (id, task, options?) => Promise<boolean> | 樂觀移除;成功真正移除、失敗還原並顫抖 |
reset | (items: T[]) => void | 以新的實體集合重置,清掉所有幽靈態 |
options 可帶 errorMessage(覆蓋 toast 文案)與 position("start" / "end",新增插入位置)。
細節
- 幽靈態:樂觀插入的項目以
opacity 0.55與虛線外框呈現,表示尚未確認;entry.pending為true。 - 凝固實體化:
taskresolve 後透明度與縮放收斂為實體態,虛線外框淡出。 - 顫抖回退:
taskreject 時外框轉紅、水平顫抖一輪,接著新增的項目移除、移除的項目還原,同時將錯誤訊息移交 toast。 - FLIP 補位:項目進出場時,其餘項目以 spring 平滑補位(
layout動畫)。 - 所有
setTimeout於卸載時清除;非同步task完成時若元件已卸載則不再更新狀態。
可及性
- 幽靈與回退中的項目帶
aria-busy="true",輔助科技可得知處理中狀態。 - 容器為
role="list"、項目為role="listitem",可傳aria-label命名清單。 - 錯誤 toast 位於
aria-live="assertive"區域並帶role="alert",附可鍵盤聚焦的關閉鈕。 - 使用者開啟「減少動態效果」時,停用進場位移、凝固縮放與顫抖,改為即時切換狀態;幽靈半透明與紅色外框等靜態提示仍保留。