nino
  • ドキュメント
  • レジストリ
  • メンバーシップ

Command Palette

Search for a command to run...

スポンサー
メニュー

並べ替え

 

2分 52秒

  • dnd-kit を使用して UI の並べ替えを実現します。
  • Fractional Indexing を使用して index の管理を実現します。
Loading...

並び替え

Loading...

なぜ Fractional Indexing を使用するのか

Fractional Indexingとは、連番ではなく、特殊な文字列で並び順を管理するライブラリです。

連番で並び順を管理する場合、並び替えの際に多くのアイテムの index を更新する必要がありますが、Fractional Indexing を使用すると、並び替えたアイテムの前後のアイテムの index を取得して、その間に新しい index を生成することができます。これにより頻繁に要素を並び替えても、データベースの更新は少なくなります。

Discord で質問する
何か気になったこと、分からないことがあれば気軽に質問してください!
DiscordDiscordで質問する
nino

Developer

XXGitHubGitHubYouTubeYouTubeZennZennDiscordDiscord

    リンク

  • ドキュメント
  • レジストリ
  • アーキテクチャ

    ツール

  • フィード
  • ステータス

    ポリシー

  • 利用規約
  • プライバシーポリシー
  • 特定商取引法に基づく表示
ドキュメント
はじめに
  • Changelog
ガイド
  • Webアプリの環境構築
  • ニュース収集&AI要約
  • Turso のテーブル移行
  • 日時の管理
  • 中規模のリストをブラウザにキャッシュし、クライアントでフィルタする
  • Search Params Dialog
  • コードの整理
  • AGENTS.md
  • Better Auth
  • プロダクト開発ポリシー
  • 推奨ツール
  • Proxy.ts
  • 多言語対応
  • SWR
  • Cache Component
  • Next.js の課題、不具合
  • アプリ開発フロー
  • プロンプトガイド
  • CSS Tips
  • Font Family
  • セルフブランディング
  • セルフオンボーディング
  • オフィスツール
  • Email
  • Breadcrumb(パンくず)
  • 並べ替え
pnpm add @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities @dnd-kit/modifiers fractional-indexing
"use client";

import {
  DndContext,
  closestCenter,
  KeyboardSensor,
  PointerSensor,
  useSensor,
  useSensors,
  DragEndEvent,
} from "@dnd-kit/core";
import {
  arrayMove,
  SortableContext,
  sortableKeyboardCoordinates,
  verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { useId, useMemo, useOptimistic, useTransition } from "react";
import { updateTask } from "@/actions/task";
import { SortableTodoItem } from "./sortable-todo-item";
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
import { Task } from "@/db";
import { generateKeyBetween } from "fractional-indexing";

export function SortableTodoList({
  tasks: initialTasks,
}: {
  tasks: Task[];
}) {
  const [isPending, startTransition] = useTransition();
  const [optimisticTasks, updateOptimisticTasks] = useOptimistic(
    tasks,
    (state, action: Task[]) => action.tasks
  );

  const sensors = useSensors(
    useSensor(PointerSensor),
    useSensor(KeyboardSensor, {
      coordinateGetter: sortableKeyboardCoordinates,
    })
  );

  function handleDragEnd(event: DragEndEvent) {
    const { active, over } = event;

    if (over && active.id !== over.id) {
      const oldIndex = optimisticTasks.findIndex(
        (task) => task.id === active.id
      );
      const newIndex = optimisticTasks.findIndex((task) => task.id === over.id);

      const newTasks = arrayMove(optimisticTasks, oldIndex, newIndex);

      // サーバーに保存
      startTransition(async () => {
        // オプティミスティック更新
        updateOptimisticTasks(newTasks);

        // 移動先の前後のタスクを取得
        const prevTask = newIndex > 0 ? newTasks[newIndex - 1] : null;
        const nextTask =
          newIndex < newTasks.length - 1 ? newTasks[newIndex + 1] : null;

        // 新しい index を計算
        const newIndexValue = generateKeyBetween(
          prevTask?.index ?? null,
          nextTask?.index ?? null
        );

        // updateTask で更新
        await updateTask(active.id as string, { index: newIndexValue });
      });
    }
  }

  // hydrationエラー対策
  const id = useId();

  return (
    <DndContext
      id={id}
      sensors={sensors}
      collisionDetection={closestCenter}
      onDragEnd={handleDragEnd}
      modifiers={[restrictToVerticalAxis]}
    >
      <SortableContext
        items={optimisticTasks.map((task) => task.id)}
        strategy={verticalListSortingStrategy}
      >
        <div>
          {optimisticTasks.map((task) => (
            <SortableTodoItem
              key={task.id}
              item={task}
              soundEnabled={settings.soundEnabled}
            />
          ))}
        </div>
      </SortableContext>
    </DndContext>
  );
}