5
3

ReactでCardをドラッグ&ドロップする

Last updated at Posted at 2023-08-31

背景

  • 2033年8月現在,IoTスタートアップ企業にてインターン活動をしています.
  • Webアプリケーション開発中にReact + TypeScriptで下図のような「Cardコンポーネントのドラッグ&ドロップ(UI) + 並べ替え結果の保存(UIの中でもフロントエンドのロジック)」機能を実装しようと考えたのですが,

Untitled.png

  1. ドラッグ&ドロップ実現用のReactライブラリはいくつかある → どれを利用すべきか
  2. ロジックの部分はどのような手段を取るべきか(データベース,Cookie etc…)
  3. 1,2を組み合わせた実装方法

なお今回の記事は1についての実装方法を記載しおります.2に関する記事は,追って投稿させていただきます.

など,実装について知見が不足していたので,公式ドキュメント,諸先輩型の記事等を探しながら学習及び実装を行いました.それらについて記事としてアウトプットすることで,学習の理解と深化,備忘録的な意味での利用を目的に本記事を書きました.

(※筆者自身学習をしながらの投稿になるので,所々文章が稚拙であったり,技術的な部分で誤りがあったりするかと思いますが,そのような箇所がございましたら,ご指摘いただけますと幸いです.)

事前学習

(ここは,結論だけ知りたい方は飛ばしていただいて構いません.)

1. ライブラリの選定

  • 結論 : clauderic/dnd-kit
  • 理由
    • ソート用の preset が用意されているので、比較的簡単に実装が可能
    • ソート時のアニメーションも標準で付与される
    • ドラッグ時にオーバーレイする要素の位置を自由にカスタマイズ可能
    • スマートフォンにも対応している
    • 公式ドキュメントが比較的わかりやすく充実している(主観)
補足 - JSでは[Sortable](https://github.com/SortableJS/Sortable)というライブラリがよく使われているらしい - ライブラリを使わずに要素のindexで並べ替える方法もある
    → [【React】ドラッグ&ドロップで並べ替えできるリストを実装する話](https://zenn.dev/totoraj930/articles/40b9fb230380a1)
    
- ただし,今回の実装では長期的なアプリ運用を見据えて,公式ドキュメントが比較的わかりやすく充実している点で,実装や更新が容易にできるので後継者たちが分かり良いのではないかと考え,ライブラリを利用することにしました.
- ライブラリ間の比較は参考文献をご覧ください.

参考文献

2. ロジックの部分はどのような手段を取るべきか

  • 結論:Local Storageにcomponentの順序を保存する(id)

  • 理由

    • 並び順を保存する方法として以下のようなものが挙げられる(補足)
      1. Local Storage (クライアント側でデータを永続的に保持できる / 5〜10MBまで)
      2. Cookie (1と似ているが,有効期限がある)
      3. データベース

    この中から,

    • 今回保存するデータ(id(number)の配列)はユーザにとって(セキュリティ上)重要な情報でない
    • 一度並べ替えたデータは永続的に保持したい
    • データベースとのやり取りなど,処理を冗長化,複雑化したくない

    の理由で,Local Storageにcomponentの順序を保存するという結論に至りました.

学習時のメモ
※字の汚さ等はご容赦ください

![IMG_0603D380D40B-1.jpeg](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b725c85e-8a2c-4587-8ddb-b750912a8a2d/IMG_0603D380D40B-1.jpeg)

参考文献
(個人的に,前者のサイトは手法の比較やセキュリティ面での注意事項まで記載があって分かりやすかったです)

番外編

今回の実装ではuseSortableというhookを使いました.しかし,その根本にはuseDraggable / useDroppable なるhooksがあるらしく,こちらも気になったので少しだけ勉強しました.

手書きのメモ(Docを読んだ時のメモ用紙)

Untitled.png

環境

  • TypeScript 4.9.4

  • React 18.2.0

  • dnd-kit (ライブラリ)

    ※ライブラリのインストール方法等に関しては,↓公式のドキュメントをご覧ください

実装

  • 基本的には公式ドキュメントを参考にしながら,以下のように実装しました.特筆事項を下に記載します.

Card Component

※Componentの実装にはmaterialUIを使っています.Propsやスタイルの設定等は割愛しますので,詳細は公式ドキュメントをご覧ください.

Card.tsx
import * as React from 'react';
import { Card } from '@mui/material';
import { useSortable } from '@dnd-kit/sortable'; //*1
import { CSS } from '@dnd-kit/utilities';

type Props = {
  id: number,
  name: string
};

var cardStyle = {
  display: 'block',
  transitionDuration: '0.3s',
}

export const Card = ({ id, name }: Props) => {
  const { attributes, listeners, setNodeRef, transform } = useSortable({
    id: id
  });
  const style = {
    transform: CSS.Transform.toString(transform)
  };

  return (
    <div ref={setNodeRef} {...attributes} {...listeners} style={style}>
      <Card>
         <p>{name}</p>
      </Card>
    </div>
  );
}

index page

index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { Card } from '../../Card';
import { Grid, Box } from '@mui/material';
import { SortableContext, arrayMove, rectSortingStrategy } from "@dnd-kit/sortable";
import { DndContext, PointerSensor, closestCenter, useSensor, useSensors } from '@dnd-kit/core';

export const Index = () => { 
  const {card_data} = ...; //id,nameを持つ連想配列を取得
 
  //  for sorting Card
  const [items, setItems] = React.useState([1, 2, 3, 4]); //*2
  const sensors = useSensors(useSensor(PointerSensor));
	
  function handleDragEnd(event) {
    const {active, over} = event;
    if (active.id !== over.id) {
      setItems((items) => {
        const oldIndex = items.indexOf(active.id);
        const newIndex = items.indexOf(over.id);
        return arrayMove(items, oldIndex, newIndex);
      });
    }
  }
   
	// 引数のidを自身のidプロパティに所有するCardComponentを取得する
  const getCard = (id) => {
    const data = card_data.find((item) => item.id === id);
    return data ? (
      <Grid item xs={4} sx={{ padding: 0 }} key={data.name}>
        <Card
					id={data.id}
          name={data.name}
        />
      </Grid>
    ) : null;
  }

  return (
    <>
    <DndContext 
      sensors={sensors}
      collisionDetection={closestCenter}
      onDragEnd={handleDragEnd}
    >
      <SortableContext 
        items={items}
        strategy={rectSortingStrategy}
      >
      <Box sx={{ flexGrow: 1 }}>
        <Grid container spacing={2}>
          items.map(id => getCard(id))} 
        </Grid>
      </Box>   
      </SortableContext>
    </DndContext>
  </>
  );
};

ReactDOM.render(<Index />, document.getElementById('Index'));

メモ

  • *1
    useSortableとは,DOM要素をドラッグ可能にするためのuseDraggable hook / useDroppable hook を抽象化し,利用するためのhookです.実装時に各Propsの役割のところで少しつまづきましたが,特段カスタムする必要はなく,便利な道具は全てライブラリ側がすでに持ってくれているので,ドキュメントを読みながら使いたい機能を探すのが手っ取り早かったです.

  • *2
    SortableContextitems propsの箇所に

    It requires that you pass it a sorted array of the unique identifiers associated with the elements that use the useSortable hook within it.

    (useSortable hook を使う要素に関連する一意な識別子をソートした配列を渡す必要がある)

    という記載があったので,Cardに渡すid属性識別子に利用しました.

結果

実際に表示させると,思惑通り動きました.

画面収録-2023-08-30-21.04.32.gif

まとめ


  • ドロップを行うためのDnDContext,SortableContextの実装でやや手こずりましたが,落ち着いてドキュメントを見返すことで,無事に実装することができました.
  • 個人的にはライブラリの選定をしている時に,下図のような類似機能実装用ライブラリの利用数の時系列データを見たり,アプリ要件にマッチしているかを考えたりする工程が初めての経験だったので,とても印象に残りました.
  • 今後の開発業務の中でもっと色々なライブラリに触れ(,あるいは自身で1から実装し)ながら技術力を高める一方で,どんなツールやライブラリ,技術がよく使われているのか,なぜ人気なのかみたいなところにも目を向けられたら,楽しみながら技術者としての引き出しが増やせて素敵だなと感じました.

Untitled.png

npm trends

最後に参考文献を記載させていただきます.

参考文献

今回の機能実装のサンプルとして参考にさせていただきました.
CodeSandbox 便利👏

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3