💨

Reactでドラッグ&ドロップをライブラリを使わずに自力で実装する

2023/11/27に公開

モチベーション

Reactの良いところは、覚えることが少ないところだと思います。
変にライブラリをごちゃごちゃ入れずとも実装できることが多いので、個人的にはわざわざライブラリを入れなくても良い実装は、自力で実装したいなと思ってます。
今回は一見自力実装が難しそうなドラッグ&ドロップをサンプルコードも合わせながら解説していきたいと思います。

コードはこちら

実装に際して知っておくべきマウスイベント

実装に際して、要件の構造分解をしてみましょう。
一口にドラッグ&ドロップといっても以下のような要素に分解できるかと思います。

  • ドラッグする(掴む)
  • 掴んだまま移動する
  • ドロップする(掴んでいた状態から離す)

実はこれらのイベントはReactで定義できます。
旧Reactドキュメントの「合成イベント (SyntheticEvent)」でその詳細が記載されています。

https://ja.legacy.reactjs.org/docs/events.html#mouse-events

onClick onContextMenu onDoubleClick onDrag onDragEnd onDragEnter onDragExit
onDragLeave onDragOver onDragStart onDrop onMouseDown onMouseEnter onMouseLeave
onMouseMove onMouseOut onMouseOver onMouseUp

上記のうち、今回着目するイベントは

  • onDragStart
  • onDragOver
  • onDrop

の3つです。
※draggableもtrueにしておいてください

実際にコードベースで実装を見ていく

実装しているのは、ドラッグ&ドロップでカードの位置を置き換える操作となってます

実際に触りながら確認してみたい方はこちらからどうぞ

関連のある箇所を部分的に抜き出しているので、コードの全容を確認したい方はGithubをご覧ください

一見、わかりにくいのですが、端的に言えばprimary-key={content.id}で定義したidを監視しているだけです


export type PositionType = {
  point: string | null;
};

// Dragが始まった時の関数
export const handleDragStart = (
  event: React.DragEvent,
  position: MutableRefObject<PositionType>
) => {
// primary-keyとして定義されているcontent.idをセット
  position.current.point = event.currentTarget.getAttribute("primary-key");
};

export const handleDragOver = (
  event: React.DragEvent,
) => {
  event.preventDefault();
 };

export const useDragAndDrop = () => {
  const [markdown, set] = useRecoilState(markdownContentTypeSelector);
    // レンダリングを抑えるためにuseRefで状態管理
  const draggingObjectState = useRef<PositionType>({
    point: null,
  });

  const dragOver = (event: DragEvent) => {
    handleDragOver(event);
  };

  const dragStart = (event: DragEvent) => {
    handleDragStart(event, draggingObjectState);
  };

  const handleDrop = (event: DragEvent) => {
    const hoveredElementPrimaryKey: string | null =
      event.currentTarget.getAttribute("primary-key");
    const draggingElementPrimaryKey: string | null =
      draggingObjectState.current.point;
    const hoveredElementIndex: number = getElementIndex(
      markdown,
      hoveredElementPrimaryKey
    );
    const draggingElementIndex: number = getElementIndex(
      markdown,
      draggingElementPrimaryKey
    );
    // 位置の置き換え
    const replaceList = reSortArrayElements(
      markdown,
      hoveredElementIndex,
      draggingElementIndex
    );
        // 状態を更新
    set(replaceList);
  };

  return { dragOver, dragStart, handleDrop };
};

export const CardSection: FC<Props> = ({
  content,
  updateContents,
  deleteContents,
  handleDrop,
  dragStart,
  dragOver,
}) => {
  const [isEdit, toggleIsEdit] = useReducer((state) => {
    return !state;
  }, false);
  const handleDelete = () => {
    content.content.length === 0
      ? updateContents({ id: content.id, content: "" })
      : deleteContents(content);
  };

  return (
    <>
      {isEdit || content.content === "# " ? (
        <div className={style.editCardSection}>
          {/* substitute DraggableIcon */}
          <DeleteIcon handleDelete={handleDelete} />
          <Spacer horizontal size={12} />
          <Card>
            <EditMode
              updateContents={updateContents}
              toggleIsEdit={toggleIsEdit}
              content={content}
            />
          </Card>
        </div>
      ) : (
        <div
          primary-key={content.id}
          key={content.id}
          draggable={true}
	  // ここでマウスイベントに対して関数を定義
          onDrop={handleDrop}
          onDragStart={dragStart}
          onDragOver={dragOver}
        >
          <div className={style.cardSection}>
            <DraggableIcon />
            <Spacer horizontal size={12} />
            <Card>
              <DisplayMode
                toggleIsEdit={toggleIsEdit}
                contentMarkdown={content.content}
              />
            </Card>
          </div>
        </div>
      )}
    </>
  );
};

※GithubでのコードではbeDraggedObjectStateという状態を定義していますが、なくても動くので上記では削除してます。

さいごに

ここまで書くならライブラリ使ったほうが良いかも、と思いました。

Discussion