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

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

前端

ThisWeb

資深前端工程師

發佈/更新於

2025年11月10日

免費訂閱電子報!

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

免費訂閱電子報!

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

甚麼是樂觀更新 Optimistic Updates?

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

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

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

這種先更新、後驗證的模式,就是樂觀更新。比如以下的 IG 按愛心功能:

在按愛心的時,IG 已經假定伺服器回應會成功,所以直接在畫面上顯示成功的 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 除了接收初始狀態,也接收第二個參數負責生成樂觀狀態。在真正的狀態完成更新前,都會顯示樂觀狀態。

實做 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 來更新狀態。

formAction 也是 React 19 更新的特性。可以參考 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 中使用樂觀更新,希望這篇能讓你更了解樂觀更新!

文章目錄

  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. 小結

頁面導覽

  • 首頁
  • 所有文章

聯絡資訊

THISWEB
01 小時 35 分鐘 07 秒
⏰ 限時 63 折優惠【React 效能優化實戰課】👉🏻 點我了解更多