ThisWeb Logo
This.Web
所有文章React 效能優化實戰課
  1. 首頁
  2. 所有文章
  3. 前端
  4. 樂觀更新 Optimistic Updates 是什麼?如何在 React 中使用樂觀更新?

樂觀更新 Optimistic Updates 是什麼?如何在 React 中使用樂觀更新?

前端

Kun | ThisWeb

資深前端工程師

發佈/更新於

2025年11月10日

免費訂閱電子報!

和 2000+ 工程師一起學習軟體、AI 開發技巧,每週一收穫 1 篇技術內容、1 段職涯分享、1 個最新資訊!

免費訂閱電子報!

和 2000+ 工程師一起學習軟體、AI 開發技巧,每週一收穫 1 篇技術內容、1 段職涯分享、1 個最新資訊!

甚麼是樂觀更新 Optimistic Updates?

在前端開發中,樂觀更新(Optimistic Updates)是一種用來提升互動流暢度與使用者體驗的技術手法。

簡單來說,它的核心思想是:「在伺服器回應之前,先假設操作會成功,並立即更新使用者介面。」

當你在社群平台按下愛心、留言、或將商品加入購物車時,畫面通常會瞬間更新,而不是等伺服器回傳結果。

這種先更新、後驗證的模式,就是樂觀更新。

比如我們在 IG 或 Threads 按愛心時,他會假定伺服器回應會成功,所以直接在畫面上顯示成功的 UI 更新,直接在圖片上覆蓋一個大大的愛心。

同時,取消愛心的時候也是,IG 也是直接設定取消愛心的伺服器回應會成功,所以在畫面上直接更新取消愛心的 UI。

若稍後伺服器回傳錯誤(例如網路中斷),則會再將狀態**回滾(rollback)**回原狀。

樂觀更新 Optimistic Updates 的優點

樂觀更新的優點是對使用者的互動體驗很好,如果我們要等待伺服器回應成功後,畫面才更新,這會讓使用者和網頁的互動有延遲感。

你能想像你在 IG 或 FB 按愛心時,等了 5 秒才更新畫面嗎?想必這樣體驗會很糟糕。

使用樂觀更新 Optimistic Updates

什麼時候適合使用樂觀更新 Optimistic Updates

以下幾種情況可以考慮樂觀更新來增加體驗:

  1. 低風險操作:操作失敗的後果並不嚴重,可以輕易還原。
  2. 需要即時反饋:一些操作需要立即給予用戶回饋,比如郵件發送成功提示、購物車更新等。
  3. 只有兩種情況時:當操作只有兩種情況的時候,例如喜歡/不喜歡,按讚/不按讚等等。
  4. 高頻率的操作場景:當用戶頻繁觸發操作時(如按愛心、收藏清單),等待回應會造成明顯延遲,樂觀更新能顯著提升順暢度。

總之,當操作風險不大時,都可以嘗試樂觀更新,像 IG 如果愛心功能失效,他只要還原 UI 即可,沒加到愛心對使用者的影響也不大。

什麼時候不適合使用樂觀更新 Optimistic Updates

那什麼時候不適合使用樂觀更新呢?

只要當操作涉及到一些嚴格要求一致性,或高風險操作的使用場景時,例如銀行轉帳、訂單支付等等,可能就不太適合使用樂觀更新,因為一旦失敗,可能會造成畫面和資料的不一致,導致一些不可預期的後果。

當回應失敗時如何處理樂觀更新?

大部分的產品在伺服器回應失敗時,會採用三種方法

  1. 直接還原本地 UI:最直接的方法是在請求失敗時,將本地狀態還原到操作之前的狀態。這種方法適用於可以輕易還原的場景,例如按讚不按讚。
  2. 顯示錯誤訊息:如果還原狀態還不夠,可以跳出錯誤訊息通知用戶。
  3. 自動重試:有些時候,產品會自動重式失敗的操作,而不是立即還原狀態。

如何在 React 中如何實現樂觀更新 - useOptimistic

在 React 中,我們可以使用 useOptimistic 來實作樂觀更新,

在開始看 useOptimistic 前,我們先將狀態分成兩種。

  1. 真正的狀態:由 useState 返回的 state
  2. 樂觀狀態:預設更新成功時的狀態,由 useOptimistic 返回的 optimisticState (樂觀狀態)。

有了基本觀念之後就可以來看 useOptimistic,他接收兩個參數,也返回兩個值。

javascript
import { useOptimistic } from 'react';

function App() {
  const [optimisticState, addOptimistic] = useOptimistic(
    state, // 接收的第一個參數:初始 state
    // 接收的第二個參數:處理樂觀更新的函數 updateFn
    (currentState, optimisticValue) => {
      // 回傳生成的樂觀狀態,當非同步處理完成後回傳真正的新值
    }
  );
}

接著讓我們詳細看看 useOptimistic 函數的參數

  • 第一個參數 state: 初始的真正狀態,通常是由 useState 返回的 state。
  • 第二個參數 updateFn(currentState, optimisticValue):是一個函數,它接受當前的真正狀態和傳遞給 addOptimistic 的樂觀值 (optimisticValue),並返回生成的樂觀狀態 (optimisticState)。

也就是說這個函數接收兩個參數,真正的狀態和樂觀狀態,並返回新的樂觀狀態。

useState 和 useOptimistic 的差別

其實 useOptimistic 和 useState 有點像,一樣回傳兩個值,第一個值是狀態,第二個值是更新狀態的函數。

javascript
const initValue = [{ todo: '買牛奶' }];

const [state, setState] = useState(initValue);
const [optimisticState, setOptimisticState] = useOptimistic(
  state,
  (currentState, optimisticValue) => {
    return [...state, { todo: optimisticValue }];
  }
);

只不過 useOptimistic 除了接收初始狀態,也接收第二個參數負責生成樂觀狀態。在真正的狀態完成更新前,都會顯示樂觀狀態。

前端升級計劃

如果你想成為更厲害的前端工程師,我製作了一堂計劃,會帶你學習 React 進階觀念、TypeScript 攻略應用、Next.js 專案開發、職場溝通術、職涯管理等一系列內容。

前端職涯升級計劃封面

可以先參考課程介紹:課程連結

如果你有興趣,但不確定適不適合你,可以先花 3 分鐘填寫下方表單預約諮詢。
這個諮詢是完全免費的,目的是幫助我了解你的學習狀況、遇到的挑戰,我也會根據經驗,帶著你規劃接下來的目標。

表單連結

實做 useOptimistic

接著讓我們看看實際應用場景,假設我們有個更新代辦清單的操作,我們希望用戶輸入完新的代辦事項後樂觀更新 UI,而不用等待伺服器回傳成功後才更新 UI。

我們先用 addTodoAction 函數來模擬非同步新增 todo。

javascript
// 模擬新增代辦事項的操作
export async function addTodoAction(todo) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.3) {
        resolve(todo);
      } else {
        reject('Failed!');
      }
    }, 1000);
  });
}

接著我們在應用程式主體先寫好基本的 form 樣式,並宣告一個 todos 狀態。

javascript
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>
    </>
  );
}

沒有樂觀更新的作法

我們可以直接在 formAction 函數裡面去 request (請求),當 request 成功後再用 setTodos 來更新狀態。

參考 React 官方的介紹

並在 JSX 裡去 map todos 來渲染所有的 todos。

javascript
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>
    </>
  );
}

使用樂觀更新的做法

但我們現在用樂觀狀態改善體驗,此時就可以引入 useOptimistic。

我們先宣告 useOptimistic,第一個參數接受由 useState 返回的 todos,第二個參數接收一個生成 optimisticTodos 的函數,也就是新增一個 sending 為 true 的 todo 物件。

javascript
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 <>{/*...*/}</>;
}

接著調整 formAction,在對伺服器請求前,先更新樂觀狀態。並在伺服器回傳成功時更新真正的 todo state,如果失敗,還原樂觀更新的狀態。

這裡用 sending 是為了更清楚看到整個流程,當送出請求的時候,會先使用樂觀狀態 (sending: ture),直到請求回傳成功後,才會更新真正的狀態 (sending: false)

javascript
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 (<>{/*...*/}</>)
}

最後,在 JSX 使用樂觀狀態的值去 map。

javascript
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>
    </>
  );
}

此時就可以體驗樂觀更新的 UI了!

如果伺服器回傳成功,就會直接用新的 state 覆蓋樂觀 state 👇

如果失敗,也會直接取消 UI 的更新。

下面附上完整的程式碼:

javascript
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>
    </>
  );
}

小結

今天介紹了何謂樂觀更新 Optimistic Update,是非常常見的技術,在某些場景下可以讓用戶的體驗大增,也介紹了怎麼在 React 中使用樂觀更新,希望這篇能讓你更了解樂觀更新!

下一篇看什麼?

01.

如何寫出好的單元測試?從測試觀念到實戰

02.

前端測試架構完整指南:單元測試、整合測試、E2E 測試與測試金字塔

03.

Next.js Page Transition 實作:整合 View Transition API 與 JavaScript 頁面轉場

文章目錄

  1. 甚麼是樂觀更新 Optimistic Updates?
  2. 樂觀更新 Optimistic Updates 的優點
  3. 使用樂觀更新 Optimistic Updates
  4. 什麼時候適合使用樂觀更新 Optimistic Updates
  5. 什麼時候不適合使用樂觀更新 Optimistic Updates
  6. 當回應失敗時如何處理樂觀更新?
  7. 如何在 React 中如何實現樂觀更新 - useOptimistic
  8. useState 和 useOptimistic 的差別
  9. 實做 useOptimistic
  10. 沒有樂觀更新的作法
  11. 使用樂觀更新的做法
  12. 小結

訂閱電子報!

和 2000+ 人一起學習 AI、軟體與網站實作資訊。

或來信合作:kun@thisweb.dev

頁面導覽

  • 首頁
  • 所有文章

聯絡資訊

THISWEB