【React】ファイルドロップをライブラリ無しで実装する


こんにちは、フリーランスエンジニアの太田雅昭です。

Reactでのファイルドロップ

Reactでファイルドロップするには、ライブラリを使うのが通常かと思います。ただライブラリをできるだけ削りたい事情がある場合、通常のAPIだけで賄いたいといったことがあります。

ファイルドロップの罠

ファイルドロップを実装する場合、以下のような流れが思い浮かびます。

  • ドロップ入ったらstateを”over”に
  • ドロップ出たらstateを”idle”に
  • ドロップイベントでファイル処理
  • あれ?簡単じゃね?

しかし単純に実装すると、以下のようになります。

  • ドロップ入ったらstateが”over”になる
  • マウスが子要素に入ったらstateが”idle”になる
  • つまりドロップゾーン内にもかかわらずstateが”idle”になってしまう

これは困りました。

動作確認

以下のようなコードで確認してみます。

export default function App() {
  return (
    <div
      onDragEnter={(e) => {
        e.preventDefault();
        console.log("onDragEnter");
      }}
      onDragLeave={(e) => {
        e.preventDefault();
        console.log("onDragLeave");
      }}
      style={{ width: 500, height: 500, background: "gray" }}
    >
      <div style={{ width: 100, height: 100, background: "red" }}>A</div>
      <div style={{ width: 100, height: 100, background: "blue" }}>B</div>
    </div>
  );
}

すると以下のようになります。

  • onDragEnterが発火
  • 子要素の行き来でonDragLeave,onDragEnterが発火

つまり全ての要素で同じイベントが発火していることになります。なるほど確かに、これだと思い通りにいかないことが明白です。

カウント対策

色々な対策方法があるかと思いますが、カウント方式が一番シンプルかと思います。具体的には、enterでインクリメント、leaveでデクリメントし、0になれば全ての要素から出たと判定します。以下のようにします。

import { useRef } from "react";

export default function App() {
  // カウント用Ref
  const dragCountRef = useRef(0);

  return (
    <div
      onDragEnter={(e) => {
        e.preventDefault();
        // enterでcountインクリメント
        dragCountRef.current++;
        console.log("onDragEnter");
      }}
      onDragLeave={(e) => {
        e.preventDefault();
        // leaveでcountデクリメント
        dragCountRef.current--;
        console.log("onDragLeave");

        if (dragCountRef.current === 0) {
          // count0で本当に外に出ている
          console.log("外に出ました!");
        }
      }}
      style={{ width: 500, height: 500, background: "gray" }}
    >
      <div style={{ width: 100, height: 100, background: "red" }}>A</div>
      <div style={{ width: 100, height: 100, background: "blue" }}>B</div>
    </div>
  );
}

シンプルで良いですね。