📝

React19で追加されたhooks触ってみた3(useOptimistic)

2025/03/02に公開

引数で受け取った値をコピーして、そのまま返す。
非同期アクションが実行中の場合には、一時的に別の値を返すことができる。

定義

const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);

引数

state

初期状態またはアクションが実行中でない場合に返却される値

updateFn(currentState, optimisticValue): state

現在のstateaddOptimisticを呼び出した際に渡した値(optimisticValue)を使用して、アクション実行中に使用するstateを返す関数
純粋関数である必要がある

返り値

optimisticState

アクション実行中以外は、引数で渡したstateになる。
アクション実行中の場合は、updateFnの返り値になる。

addOptimistic

楽観的な更新を行う際に呼び出すディスパッチ関数
任意の型(updateFnoptimisticValueと同様の型)の引数を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状態を更新してからサーバー側の状態を更新するようなケースでは便利だと思った。

参考

https://ja.react.dev/reference/react/useOptimistic

Discussion