React19のuseOptimisticとDnDを組みせるといい感じ
React19のuseOptimisticの使いどころがそんなにわからないなと思っていた昨今ですが、めっちゃハマる使いどころを見つけたので共有します!
useOptimisticってなに?
「楽観的更新をするためのHook」と記載されており、「データの更新→ロード→更新されたあとのデータを表示」の動作のロードの部分を割愛するための仕組みで、確実にデータの更新ができるであろうケースに使用するとアプリを高速で操作できるように見せることができるようです。
useOptimisticの使用の有無の比較
なし
あり
なしのほうがアイテムを離したあとに少しカクついていることがわかります。
DnDでデータを並び替える実装をみる(なし)
ドラックアンドドロップ(以下DnD)で操作を行う場合の多くはDropしたときにDBのデータも書き換えたいものだと思います。今回はリストを並び替えるという動作を例にあげます。
このような実装ではUIに実データをそのまま使用してしまうがちですが、それだとViewで使用しているデータが不整合になる時間が生じてしまい、リアルタイム制が高いDnDの実装だと、先程示したようなカクつきが生まれてしまいます。
DnDでリストを並び替えるコンポーネントをざっと作るとこんな感じです。
※今回はdnd-kitを使用しています。
'use client';
import {
DndContext,
type DragEndEvent,
KeyboardSensor,
PointerSensor,
closestCenter,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
SortableContext,
arrayMove,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
type Props = {
items: Item[]; // APIなどで取得した実データ
};
const List = ({ items }: Props) => {
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const handleDragEnd = async(event: DragEndEvent) => {
const { active, over } = event;
if (over === null) return;
if (active.id !== over.id) {
const oldIndex = lists.findIndex((item) => item.id === active.id);
const newIndex = lists.findIndex((item) => item.id === over.id);
await setItems(arrayMove(items, oldIndex, newIndex)); // 保存する処理(非同期)
}
};
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={items}
strategy={verticalListSortingStrategy}
>
<div>
{items.map((item) => (
<ListItem key={item.id} id={item.id} item={item} />
))}
</div>
</SortableContext>
</DndContext>
);
};
挙動はデータの更新がされるまでは順番がわかっていないので、一瞬ListItem
が元いた場所に戻ろうとしてしまいます。
この解決方法はいくらか想像つくと思いますが、React19からは、useOptimistic
を使うのが良さそうです。
useOptimistic
を使ったDnDの実装をみる(あり)
今回の並び替えの処理は失敗する可能性が低そうなので、useOptimistic
を使用して楽観的更新をします。
'use client';
import {
DndContext,
type DragEndEvent,
KeyboardSensor,
PointerSensor,
closestCenter,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
SortableContext,
arrayMove,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { startTransition, useOptimistic } from 'react';
type Props = {
items: Item[]; // APIなどで取得した実データ
};
const List = ({ items }: Props) => {
const [optimisticItems, setOptimisticItems] = useOptimistic(
lists, // 実データを渡す
(_currentItems, newItems: Item[]) => newItems, // 更新関数(今回は前回の値 _currentItems は使用しない)
);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (over === null) return;
if (active.id !== over.id) {
const oldIndex = lists.findIndex((item) => item.id === active.id);
const newIndex = lists.findIndex((item) => item.id === over.id);
// ※useOptimistic は startTransition,もしくはserver functionといっしょに使用すること
startTransition(async () => {
setOptimisticItems(arrayMove(optimisticLists, oldIndex, newIndex));
await setItems(arrayMove(lists, oldIndex, newIndex));
});
}
};
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={optimisticItems} strategy={verticalListSortingStrategy}>
<div>
{optimisticItems.map((item) => (
<ListItem key={item.id} id={item.id} item={item} />
))}
</div>
</SortableContext>
</DndContext>
);
};
補足
const [optimisticItems, setOptimisticItems] = useOptimistic(
lists, // 実データを渡す
(_currentItems, newItems: Item[]) => newItems, // 更新関数(今回は前回の値 _currentItems は使用しない)
);
ここで実データをuseState
に渡すみたいなことをしています。optimisticItems
はclientで使用するstateです。更新関数は自由に組めるのでuseReducer
みたいな感じと捉えるといいでしょう。
startTransition(async () => {
setOptimisticItems(arrayMove(optimisticLists, oldIndex, newIndex)); // optimisticItemsの更新
await setItems(arrayMove(lists, oldIndex, newIndex)); // DBの更新
});
そして、useOptimistic
の更新関数を、DBを更新する処理と一緒に呼び出してあげましょう。これで、optimisticItems
の更新とDBの更新が行われclientではoptimisticItems
をViewに使用しているので動きがいい感じになったというわけです。
また、この更新の処理にはstartTransition
を使用する、更新処理にserver function (action)を使用するようにしましょう。
というわけで、かなりきれいにDnDで並び替えすることができました🎉
ブラウザを更新するとDBの値がちゃんと書き換わっていることを確認できました!
useOptimistic
はシンプルなCRUD処理でガンガン使って良さそうだと思いました!
仮にエラーになった場合はToastやスナックバーなどを表示して失敗したことをユーザーに伝えればいいのかなと思います!
Discussion