前端框架
-React useOptimistic Hook 詳解 - 如何在 React 實作 Optimistic Update?
React 19 推出了一個新的 Hook,專門用來樂觀更新 - useOptimistic。如果你不知道甚麼是樂觀更新,可以參考我寫的這篇文章,裡面有詳細的介紹。
在 React 中如何實現樂觀更新 - useOptimistic
在開始看 useOptimistic 前,我們先將狀態分成兩種。
- 真正的狀態:由 useState 返回的 state
- 樂觀狀態:預設更新成功時的狀態,由 useOptimistic 返回的 optimisticState (樂觀狀態)。
有了基本觀念之後就可以來看 useOptimistic,他接收兩個參數,也返回兩個值。
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 有點像,一樣回傳兩個值,第一個值是狀態,第二個值是更新狀態的函數。
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。
// 模擬新增代辦事項的操作
export async function addTodoAction(todo) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.3) {
resolve(todo);
} else {
reject("Failed!");
}
}, 1000);
});
}
接著我們在應用程式主體先寫好基本的 form 樣式,並宣告一個 todos 狀態。
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。
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 物件。
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)
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。
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 的更新。
下面附上完整的程式碼,也可以參考完整程式碼示範。
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>
</>
);
}
小結
今天使用了 React 19 新的 Hook useOptimistic
,React 19 更新了許多異步操作的 Hook,很推薦去看看,這些 Hook 都可以幫助我們打造體驗更好的應用程式。