📝
React19で追加されたhooks触ってみた3(useOptimistic)
引数で受け取った値をコピーして、そのまま返す。
非同期アクションが実行中の場合には、一時的に別の値を返すことができる。
定義
const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);
引数
state
初期状態またはアクションが実行中でない場合に返却される値
updateFn(currentState, optimisticValue): state
現在のstate
とaddOptimistic
を呼び出した際に渡した値(optimisticValue
)を使用して、アクション実行中に使用するstate
を返す関数
純粋関数である必要がある
返り値
optimisticState
アクション実行中以外は、引数で渡したstate
になる。
アクション実行中の場合は、updateFn
の返り値になる。
addOptimistic
楽観的な更新を行う際に呼び出すディスパッチ関数
任意の型(updateFn
のoptimisticValue
と同様の型)の引数を1つ受け取る。この関数を呼び出すと現在のstate
と、この関数に渡した値を使用して、updateFn
が呼び出される。
使用例
Todo追加処理で使用
type Todo = {
id: string;
text: string;
isAdding: boolean;
};
const addTodo = async (text: string) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return {
id: self.crypto.randomUUID(),
text,
isAdding: false,
};
};
const TodoList = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [inputValue, setInputValue] = useState("");
const [optimisticTodos, addOptimisticTodo] = useOptimistic<Todo[], string>(
todos,
(state, newTodoText) => [
...state,
{
id: "optimistic-" + self.crypto.randomUUID(),
text: newTodoText,
isAdding: true,
},
]
);
const handleAddTodo = async () => {
addOptimisticTodo(inputValue);
const newTodo = await addTodo(inputValue);
setTodos((cur) => [...cur, newTodo]);
setInputValue("");
};
return (
<div>
<form action={handleAddTodo}>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.currentTarget.value)}
placeholder="新しいタスク"
/>
<button type="submit">追加</button>
</form>
<div>
タスク一覧
<ul>
{optimisticTodos.map((todo) => (
<li key={todo.id}>
<span>{todo.text}</span>
{todo.isAdding && <span> (追加中...)</span>}
</li>
))}
</ul>
</div>
</div>
);
};
export default TodoList;
ToDo状態更新時に使用
type Todo = {
id: string;
text: string;
isCompleted: boolean;
isPending: boolean;
};
const updateIsCompleted = async (id: string, isCompleted: boolean) => {
console.log(`updateIsCompleted: ${id}, ${isCompleted}`);
await new Promise((resolve) => setTimeout(resolve, 1000));
// サーバーに送信する処理
};
const TodoList = () => {
const [todos, setTodo] = useState<Todo[]>([
{
id: "1",
text: "Test1",
isCompleted: false,
isPending: false,
},
{
id: "2",
text: "Test2",
isCompleted: false,
isPending: false,
},
]);
const [optimisticTodos, updateOptimisticTodos] = useOptimistic<
Todo[],
string
>(todos, (state, id) =>
state.map((todo) =>
todo.id === id
? { ...todo, isCompleted: !todo.isCompleted, isPending: true }
: todo
)
);
const handleToggleTodo = async (id: string) => {
const targetTodo = todos.find((todo) => todo.id === id);
if (!targetTodo) return;
updateOptimisticTodos(id);
await updateIsCompleted(targetTodo.id, !targetTodo.isCompleted);
setTodo((cur) =>
cur.map((todo) =>
todo.id === id
? { ...todo, isCompleted: !todo.isCompleted, isPending: false }
: todo
)
);
};
return (
<div>
<div>
タスク
{optimisticTodos.map((todo) => (
<div key={todo.id}>
<form
action={async () => {
await handleToggleTodo(todo.id);
}}
>
{/* checkbox風のボタン */}
<button
type="submit"
style={{
marginTop: "8px",
display: "flex",
alignItems: "center",
background: "none",
border: "none",
padding: "0",
width: "100%",
textAlign: "left",
cursor: "pointer",
}}
role="checkbox"
>
<span
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: "20px",
height: "20px",
border: `2px solid ${
todo.isCompleted ? "#4c7dff" : "#666"
}`,
borderRadius: "4px",
marginRight: "8px",
color: "white",
backgroundColor: todo.isCompleted
? "#4c7dff"
: "transparent",
}}
>
{todo.isCompleted && "✓"}
</span>
<span>
{todo.text} {todo.isPending && "(更新中...)"}
</span>
</button>
</form>
</div>
))}
</div>
</div>
);
};
export default TodoList;
感想
最初は他に追加されたフックに比べて、使い道がないと思っていたが、X(旧Twitter)のお気に入りボタンのような機能を実装する際に、先にUI状態を更新してからサーバー側の状態を更新するようなケースでは便利だと思った。
参考
Discussion