甚麼是樂觀更新 Optimistic Updates?
在前端開發中,樂觀更新(Optimistic Updates)是一種用來提升互動流暢度與使用者體驗的技術手法。
簡單來說,它的核心思想是:「在伺服器回應之前,先假設操作會成功,並立即更新使用者介面。」
ThisWeb
資深前端工程師
發佈/更新於
和 2000+ 工程師一起學習軟體、AI 開發技巧,每週一收穫 1 篇技術內容、1 段職涯分享、1 個最新資訊!
在前端開發中,樂觀更新(Optimistic Updates)是一種用來提升互動流暢度與使用者體驗的技術手法。
簡單來說,它的核心思想是:「在伺服器回應之前,先假設操作會成功,並立即更新使用者介面。」
當你在社群平台按下愛心、留言、或將商品加入購物車時,畫面通常會瞬間更新,而不是等伺服器回傳結果。
這種先更新、後驗證的模式,就是樂觀更新。
比如我們在 IG 或 Threads 按愛心時,他會假定伺服器回應會成功,所以直接在畫面上顯示成功的 UI 更新,直接在圖片上覆蓋一個大大的愛心。
同時,取消愛心的時候也是,IG 也是直接設定取消愛心的伺服器回應會成功,所以在畫面上直接更新取消愛心的 UI。
若稍後伺服器回傳錯誤(例如網路中斷),則會再將狀態**回滾(rollback)**回原狀。
樂觀更新的優點是對使用者的互動體驗很好,如果我們要等待伺服器回應成功後,畫面才更新,這會讓使用者和網頁的互動有延遲感。
你能想像你在 IG 或 FB 按愛心時,等了 5 秒才更新畫面嗎?想必這樣體驗會很糟糕。
以下幾種情況可以考慮樂觀更新來增加體驗:
總之,當操作風險不大時,都可以嘗試樂觀更新,像 IG 如果愛心功能失效,他只要還原 UI 即可,沒加到愛心對使用者的影響也不大。
那什麼時候不適合使用樂觀更新呢?
只要當操作涉及到一些嚴格要求一致性,或高風險操作的使用場景時,例如銀行轉帳、訂單支付等等,可能就不太適合使用樂觀更新,因為一旦失敗,可能會造成畫面和資料的不一致,導致一些不可預期的後果。
大部分的產品在伺服器回應失敗時,會採用三種方法
在 React 中,我們可以使用 useOptimistic 來實作樂觀更新,
在開始看 useOptimistic 前,我們先將狀態分成兩種。
有了基本觀念之後就可以來看 useOptimistic,他接收兩個參數,也返回兩個值。
接著讓我們詳細看看 useOptimistic 函數的參數
state: 初始的真正狀態,通常是由 useState 返回的 state。updateFn(currentState, optimisticValue):是一個函數,它接受當前的真正狀態和傳遞給 addOptimistic 的樂觀值 (optimisticValue),並返回生成的樂觀狀態 (optimisticState)。也就是說這個函數接收兩個參數,真正的狀態和樂觀狀態,並返回新的樂觀狀態。
其實 useOptimistic 和 useState 有點像,一樣回傳兩個值,第一個值是狀態,第二個值是更新狀態的函數。
只不過 useOptimistic 除了接收初始狀態,也接收第二個參數負責生成樂觀狀態。在真正的狀態完成更新前,都會顯示樂觀狀態。
接著讓我們看看實際應用場景,假設我們有個更新代辦清單的操作,我們希望用戶輸入完新的代辦事項後樂觀更新 UI,而不用等待伺服器回傳成功後才更新 UI。
我們先用 addTodoAction 函數來模擬非同步新增 todo。
接著我們在應用程式主體先寫好基本的 form 樣式,並宣告一個 todos 狀態。
我們可以直接在 formAction 函數裡面去 request (請求),當 request 成功後再用 setTodos 來更新狀態。
參考 React 官方的介紹
並在 JSX 裡去 map todos 來渲染所有的 todos。
但我們現在用樂觀狀態改善體驗,此時就可以引入 useOptimistic。
我們先宣告 useOptimistic,第一個參數接受由 useState 返回的 todos,第二個參數接收一個生成 optimisticTodos 的函數,也就是新增一個 sending 為 true 的 todo 物件。
接著調整 formAction,在對伺服器請求前,先更新樂觀狀態。並在伺服器回傳成功時更新真正的 todo state,如果失敗,還原樂觀更新的狀態。
這裡用 sending 是為了更清楚看到整個流程,當送出請求的時候,會先使用樂觀狀態 (sending: ture),直到請求回傳成功後,才會更新真正的狀態 (sending: false)
最後,在 JSX 使用樂觀狀態的值去 map。
此時就可以體驗樂觀更新的 UI了!
如果伺服器回傳成功,就會直接用新的 state 覆蓋樂觀 state 👇
如果失敗,也會直接取消 UI 的更新。
下面附上完整的程式碼:
今天介紹了何謂樂觀更新 Optimistic Update,是非常常見的技術,在某些場景下可以讓用戶的體驗大增,也介紹了怎麼在 React 中使用樂觀更新,希望這篇能讓你更了解樂觀更新!
import { useOptimistic } from 'react';
function App() {
const [optimisticState, addOptimistic] = useOptimistic(
state, // 接收的第一個參數:初始 state
// 接收的第二個參數:處理樂觀更新的函數 updateFn
(currentState, optimisticValue) => {
// 回傳生成的樂觀狀態,當非同步處理完成後回傳真正的新值
}
);
}const initValue = [{ todo: '買牛奶' }];
const [state, setState] = useState(initValue);
const [optimisticState, setOptimisticState] = useOptimistic(
state,
(currentState, optimisticValue) => {
return [...state, { todo: optimisticValue }];
}
);// 模擬新增代辦事項的操作
export async function addTodoAction(todo) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.3) {
resolve(todo);
} else {
reject('Failed!');
}
}, 1000);
});
}import { useState, useRef } from 'react';
// 應用程式主體
export default function App() {
const formRef = useRef();
const [todos, setTodos] = useState([{ text: '買牛奶', sending: false }]);
const formAction = async (formData) => {
//...
};
return (
<>
<h1>你的代辦清單</h1>
<ul>{/* 你的代辦清單 */}</ul>
<br />
<form action={formAction} ref={formRef}>
<input type="text" name="todo" placeholder="輸入代辦事項" />
<button type="submit">新增</button>
</form>
</>
);
}import { useState, useRef } from 'react';
// 應用程式主體
export default function App() {
const formRef = useRef();
const [todos, setTodos] = useState([{ text: '買牛奶', sending: false }]);
// 👇 formAction
const formAction = async (formData) => {
const newTodo = formData.get('todo');
formRef.current.reset();
try {
const response = await addTodoAction(newTodo);
setTodos((prevTodos) => [
...prevTodos,
{ text: response, sending: false },
]);
} catch (err) {
alert('代辦事項添加失敗');
}
};
return (
<>
<h1>你的代辦清單</h1>
<ul>
{/* 👇 用 map 去渲染所有的 todo */}
{todos.map((todo, index) => (
<li key={index}>{todo.text}</li>
))}
</ul>
<br />
<form action={formAction} ref={formRef}>
<input type="text" name="todo" placeholder="輸入代辦事項" />
<button type="submit">新增</button>
</form>
</>
);
}import { useOptimistic, useState, useRef } from 'react';
// 模擬新增代辦事項的操作
export async function addTodoAction(todo) {
//...
}
// 應用程式主體
export default function App() {
// ...
// 👇 useOptimistic
const [optimisticTodos, setOptimisticTodos] = useOptimistic(
todos, // 初始狀態
// 會先生成樂觀狀態,直到新的 todos state 更新後才會取代樂觀狀態
(currentTodos, newTodo) => [
...currentTodos,
{
text: newTodo,
sending: true,
},
]
);
//...
return <>{/*...*/}</>;
}import { useOptimistic, useState, useRef } from "react";
// 模擬新增代辦事項的操作
export async function addTodoAction(todo) {
//...
}
// 應用程式主體
export default function App() {
// ...
const [optimisticTodos, setOptimisticTodos] = useOptimistic(...);
const formAction = async (formData) => {
const newTodo = formData.get("todo");
//👇 樂觀更新 UI
setOptimisticTodos(newTodo);
formRef.current.reset();
// 👇 回傳成功後,用真正的資料更新的 UI
try {
const response = await addTodoAction(newTodo);
setTodos((prevTodos) => [
...prevTodos,
{ text: response, sending: false },
]);
} catch (err) {
// 👇 如果失敗,就將 todo state 還原,用來還原 optimisticTodos
alert("代辦事項添加失敗");
setTodos((prevTodos) => [...prevTodos]);
}
};
//...
return (<>{/*...*/}</>)
}import { useOptimistic, useState, useRef } from "react";
//...
return (
<>
<h1>你的代辦清單</h1>
<ul>
{/* 👇 map 樂觀狀態 */}
{optimisticTodos.map((todo, index) => (
<li key={index}>
{todo.text}
{/* 👇 當真正狀態更新前,會顯示由 useOptimistic 生成的樂觀狀態*/}
{/* 也就是 todo.sending 會是 true*/}
{!!todo.sending && <span>(Sending...)</span>}
</li>
))}
</ul>
<br />
<form action={formAction} ref={formRef}>
<input type="text" name="todo" placeholder="輸入代辦事項" />
<button type="submit">新增</button>
</form>
</>
);
}import { useOptimistic, useState, useRef } from 'react';
// 模擬新增代辦事項的操作
export async function addTodoAction(todo) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.3) {
resolve(todo);
} else {
reject('Failed!');
}
}, 1000);
});
}
// 應用程式主體
export default function App() {
const formRef = useRef();
const [todos, setTodos] = useState([{ text: '買牛奶', sending: false }]);
// useOptimistic
const [optimisticTodos, setOptimisticTodos] = useOptimistic(
todos, // 初始狀態
// 會先生成樂觀狀態,直到新的 todos state 更新後才會取代樂觀狀態
(currentTodos, newTodo) => [
...currentTodos,
{
text: newTodo,
sending: true,
},
]
);
const formAction = async (formData) => {
const newTodo = formData.get('todo');
// 樂觀更新 UI
setOptimisticTodos(newTodo);
formRef.current.reset();
// 回傳成功後,用真正的資料更新的 UI
try {
const response = await addTodoAction(newTodo);
setTodos((prevTodos) => [
...prevTodos,
{ text: response, sending: false },
]);
} catch (err) {
// 如果失敗,就將 todo state 還原,用來還原 optimisticTodos
alert('代辦事項添加失敗');
setTodos((prevTodos) => [...prevTodos]);
}
};
return (
<>
<h1>你的代辦清單</h1>
<ul>
{/* map 樂觀狀態 */}
{optimisticTodos.map((todo, index) => (
<li key={index}>
{todo.text}
{/* 當真正狀態更新前,會顯示由 useOptimistic 生成的樂觀狀態*/}
{/* 也就是 todo.sending 會是 true*/}
{!!todo.sending && <span>(Sending...)</span>}
</li>
))}
</ul>
<br />
<form action={formAction} ref={formRef}>
<input type="text" name="todo" placeholder="輸入代辦事項" />
<button type="submit">新增</button>
</form>
</>
);
}