ドラッグ&ドロップ編集で状態が壊れる問題をどう直したか

見た目は並び替えできるのに保存結果がずれる不具合に対し、暫定状態と確定状態の責務を分離して再発を抑えた実装ログです。

はじめに

記録時点: 2024-09〜2024-11

ドラッグ&ドロップは、動いて見えるだけでは品質を担保できません。 この時期に詰まったのは、並び替え後の保存結果が不安定になる問題でした。

この記事で扱う範囲

  • 連続操作時のみ発生する状態衝突
  • 切り分けで見えた責務混在
  • 暫定状態と確定状態を分ける修正

症状の出方

単発の並び替えでは問題が見えません。 崩れるのは、並び替えと選択変更を続けたときでした。

  • 並び替え後に別要素が選択されたように見える
  • 画面表示の順序と保存順序がずれる
  • 連続操作で重複や欠落が発生する

切り分けで分かったこと

本質は、ドラッグ中の状態と保存状態を同時に更新していたことです。 更新順が競合すると、最終状態が非決定になります。

観測した比較点

  • ドラッグ開始時の配列
  • ドロップ直後の配列
  • 保存直前の配列

この3点を比較して、ドロップ後同期処理の多重実行を特定しました。

実装で変えたポイント

1. 並び替えの確定を1経路に限定した

ドラッグ中は表示用状態だけ更新し、確定更新はドロップ完了時の1回に限定しました。

function applyDragPreview(state: EditorState, move: DragMove): EditorState {
  return { ...state, previewItems: reorder(state.previewItems, move) };
}
 
function commitDrag(state: EditorState): EditorState {
  return { ...state, savedItems: state.previewItems, version: state.version + 1 };
}

2. 選択管理をインデックス依存から外した

配列番号ではなく要素識別子で選択状態を管理し、並び替え後も選択がぶれないようにしました。

3. 連続確定の競合を抑止した

確定処理が終わる前に次の確定処理を受け付けない制御を入れています。

if (state.isCommitting) return state;
state.isCommitting = true;

見送った案

1. 保存時に毎回全件再構築する

不整合は減りますが、操作感が悪化し、別の遅延問題を招くため見送りました。

2. 並び替え完了までUI全体をロックする

安全ですが操作体験が悪く、実運用で受け入れにくいため採用していません。

3. 画面表示だけ合わせて保存側は後で修正する

根本原因を残すため、不採用です。

回帰で固定した観点

観点 期待値
単発並び替え 表示順と保存順が一致
連続並び替え 重複・欠落が出ない
並び替え後の選択 意図した要素を維持
保存→再読込 順序が維持される

この回の判断が次に効いたこと

責務分離の方針は、次の改善にもそのままつながっています。

まとめ

この修正で、DnDは見た目の操作感だけでなく保存結果まで安定しました。 操作UIの問題は「表示の違和感」ではなく「状態整合の破綻」として扱うほうが再発を減らせます。

公開リンク