🔄

【Next.js × DnD-kit】DnD-kitが“浮くのに動かない”問題の解決ガイド(安定化編)

に公開

はじめに

看護業務でつかえるかなと、日々学習しながら「看護師向け計算ツールアプリ」を Next.js + TypeScript で開発に挑戦しています。

前回の記事では、DnD-kitでスマホ長押し対応の並び替え機能を実装しました。
しかし実際に動かすと、こうなりました‥‥‥👇

  • 長押しするとボタンが“浮く”
  • でも 動かない…
  • 他のボタンが よけない…
  • 順番が 変わらない…

これは DnD-kit で最も多い “つまずきポイント” であり、原因は複数が絡むため、表面的にコードを見ても気付きにくい問題らしく、まんまとはまりました!(笑)

この記事では、

  • なぜ「浮くのに動かない」のか
  • 原因をどう切り分けるか
  • 根本的に安定させる“最終レイアウト”とは?
  • スマホでも確実に動くDnDの実装方法

をまとめて解説します。

🧠 結論:DnD-kitが動かない原因は 4つの構造問題

原因 症状 必要な対策
① id 型不一致 active.id と over.id が一致せず、順番更新できない id を string に統一
② flex / grid の自動整列 “見た目”と“座標”がズレ、順番計算が破綻 verticalListSortingStrategy + columns
③ transform / ref の適用ミス 浮くだけで座標移動しない setNodeRef と transform を正しく適用
④ grid / flex-wrap で座標取得ズレ 特にスマホで不安定 並び替えモード時だけ縦リスト化

最終的な安定構成は👇

✔ 結論:

「縦方向リスト × columnsレイアウト × verticalListSortingStrategy」
が私の実装では最も安定しました。

🩺 ① idがstringで揃っていない(最も多い原因)

DnD-kit は内部で

active.id === over.id

を厳密比較(===)します。

なので、

1 !== "1"

となり、座標は動いていても並び替えが確定できません

🔧 改善(idを完全にstring化)

const calculators = [
  { id: "medication", name: "投薬計算" },
  { id: "drip", name: "点滴速度" },
];

または初期化時に変換👇

const calculatorsWithStringId = calculators.map(c => ({
  ...c,
  id: String(c.id),
}));

export const DEFAULT_ORDER = calculatorsWithStringId.map(c => c.id);

💥 ② flex / grid だと「座標判定」が破綻する

DnD-kit は「見た目の並び」ではなく
DOMの矩形(x, y, width, height) をもとにソートします。

flex や grid ではブラウザが自動的に折り返しするため、
DnD-kitが読んでいる座標と見た目がズレます。

その結果:

  • ボタンが浮くのに動かない
  • 他のボタンがよけない
  • 順番が変わらない

といった現象が起きます。

🧩 ③ transform / ref を正しく適用していない

DnD-kit の動作は transform: translate3d() を適用することで成立します。

const { attributes, listeners, setNodeRef, transform, transition } =
  useSortable({ id });

正しい構造👇

<div
  ref={setNodeRef}
  style={{
    transform: CSS.Transform.toString(transform),
    transition,
  }}
  {...attributes}
  {...listeners}
>
  <CalcButton id={id} />
</div>

❌ よくあるミス

  • ref をボタンに渡してしまう
  • transform を自作CSSで上書きする

これをやると「浮くだけで動かない」状態になります。

🧠 ④ grid構造のままだとスマホで特に不安定

(→ columns + verticalListSortingStrategy で解決)

スマホで最も問題になるのが grid / flex-wrap のズレ です。

DnD-kit 公式でも

可変レイアウト(wrap/grid)では座標取得が不安定になる

と明記されています。

🎉 解決:並び替えモード時だけ columnsレイアウト に変更する

ここが今回の最終安定解法でした。

✔ 並び替えモード外→普通の2列UI

✔ 並び替えモード中→縦方向の columns に切り替え

(verticalListSortingStrategy と相性がよい)

🔧 最終レイアウト(スマホ2列/PC3列)

<SortableContext
  items={order}
  strategy={verticalListSortingStrategy}
>
  <ul className="columns-1 sm:columns-2 md:columns-3 space-y-3">
    {items.map(item => (
      <SortableButton key={item.id} id={item.id} />
    ))}
  </ul>
</SortableContext>

メリット👇

  • スマホ:1〜2列でも安定してDnD
  • PC:3列でも動く
  • 順番が確実に変わる
  • 物理シミュレーションが崩れない

🧪 handleDragEnd(最終形)

const handleDragEnd = (event: DragEndEvent) => {
  const { active, over } = event;
  if (!over || active.id === over.id) return;

  const oldIndex = order.indexOf(String(active.id));
  const newIndex = order.indexOf(String(over.id));
  const newOrder = arrayMove(order, oldIndex, newIndex);

  setOrder(newOrder);
  saveOrder(newOrder);
};

💾 localStorage(永続化)

// 保存
export const saveOrder = (order: string[]) => {
  if (typeof window === "undefined") return;
  localStorage.setItem("calcOrder", JSON.stringify(order));
};

// 読み込み
export const loadOrder = () => {
  if (typeof window === "undefined") return [];
  const data = localStorage.getItem("calcOrder");
  return data ? JSON.parse(data) : [];
};

初回読み込み
useEffect(() => {
  const saved = loadOrder();
  setOrder(saved.length ? saved : DEFAULT_ORDER);
}, []);

📱 optional: delay / tolerance

※次回の記事で詳細に触れるため、ここでは必要最小限だけ。

const sensors = useSensors(
  useSensor(PointerSensor, {
    activationConstraint: {
      delay: 150,
      tolerance: 5,
    },
  })
);

🧠 まとめ:DnD-kitを安定させる“処方箋”

問題 原因 確実な解決策
浮くのに動かない id不一致 id を string に統一
順番かわらない layoutの自動整列 columns + verticalListSortingStrategy
他がよけない transform未適用 setNodeRef+transformを正しく適用
スマホ不安定 rect座標ズレ columns で縦方向に並べる

🔗 シリーズ一覧

🧩【Next.js × DnD-kit】スマホ対応ドラッグ&ドロップ実装シリーズ(全3回)

1.Next.js × DnD-kitで作るスマホ対応ドラッグ&ドロップ(基礎編)
2.DnD-kitが“浮くのに動かない”問題の解決ガイド(安定化編)←(この記事)
3.スマホUX最適化&並び順を保存する仕組み(実践編)

Discussion