Reactマウスイベント完全ガイド:クリックからドラッグまで【初心者向け】

JavaScript

Webサイトでボタンをクリックしたり、画像の上にマウスを乗せると色が変わったりする動作、不思議に思ったことはありませんか?

この「ユーザーの操作に反応する仕組み」を作るのがマウスイベントです。Reactでは、簡単にマウスイベントを扱えるんですよ。

今回は、Reactでのマウスイベントの使い方を、初心者の方でも分かりやすく解説していきます。クリック、ホバー、ドラッグなど、様々な操作を実装できるようになりましょう!


スポンサーリンク
  1. マウスイベントとは?基本を理解しよう
    1. イベントの種類
    2. Reactでの基本的な書き方
  2. 主要なマウスイベント一覧
    1. クリック系イベント
    2. マウス移動系イベント
    3. マウスボタン系イベント
  3. 実例:シンプルなクリックカウンター
    1. 基本的なクリックカウンター
    2. 説明
  4. イベントオブジェクトの活用
    1. イベントオブジェクトとは
    2. 主要なプロパティ
    3. 実例:クリック位置を表示
  5. ホバーエフェクトの実装
    1. 基本的なホバーエフェクト
    2. 複数の要素のホバー管理
  6. ドラッグ&ドロップの実装
    1. シンプルなドラッグ可能な要素
  7. 合成イベント(SyntheticEvent)の理解
    1. 合成イベントとは
    2. ネイティブイベントへのアクセス
  8. イベントの伝播制御
    1. イベントバブリング
    2. 伝播を止める
    3. デフォルト動作を防ぐ
  9. パフォーマンス最適化
    1. useCallbackでメモ化
    2. スロットリング(間引き)
    3. デバウンス(遅延実行)
  10. TypeScriptでの型定義
    1. 基本的な型指定
    2. 主要な型
    3. イベントハンドラの型
    4. カスタムフックでの型定義
  11. 実用的なパターン集
    1. パターン1:長押し検出
    2. パターン2:カスタムコンテキストメニュー
    3. パターン3:マウスフォロー要素
  12. よくあるトラブルと解決法
    1. 問題1:イベントが発火しない
    2. 問題2:ホバー時のちらつき
    3. 問題3:ドラッグ中にテキストが選択される
    4. 問題4:モバイルで動作しない
  13. よくある質問
  14. まとめ:マウスイベントを使いこなそう

マウスイベントとは?基本を理解しよう

マウスイベントは、ユーザーがマウスで行う操作を検知する仕組みです。

イベントの種類

マウスには、色々な操作がありますよね。

主な操作:

  • クリック(押して離す)
  • ダブルクリック(2回連続でクリック)
  • ホバー(マウスを乗せる)
  • ドラッグ(押しながら移動)
  • 右クリック

それぞれに対応するイベントが用意されています。

Reactでの基本的な書き方

HTMLとの違い:

通常のHTMLでは、こう書きます。

<button onclick="handleClick()">クリック</button>

Reactでは:

<button onClick={handleClick}>クリック</button>

微妙な違いですが、大切なポイントがあります。

  • onclickonClick(キャメルケース)
  • "handleClick()"{handleClick}(関数を直接渡す)

主要なマウスイベント一覧

Reactで使える主なマウスイベントを見ていきましょう。

クリック系イベント

onClick

最も基本的なイベントです。マウスをクリックしたときに発火します。

<button onClick={() => console.log('クリックされた')}>
  クリックしてね
</button>

onDoubleClick

ダブルクリックされたときに発火します。

<div onDoubleClick={() => console.log('ダブルクリック')}>
  ダブルクリックしてください
</div>

onContextMenu

右クリック(コンテキストメニュー)で発火します。

<div onContextMenu={(e) => {
  e.preventDefault();
  console.log('右クリックされた');
}}>
  右クリックしてみて
</div>

マウス移動系イベント

onMouseEnter

マウスが要素の上に入ってきたときに発火しますよ。

<div onMouseEnter={() => console.log('マウスが入った')}>
  ホバーエリア
</div>

onMouseLeave

マウスが要素から出ていったときに発火します。

<div onMouseLeave={() => console.log('マウスが出た')}>
  ホバーエリア
</div>

onMouseOver / onMouseOut

onMouseEnter/onMouseLeaveと似ていますが、子要素でも発火するという違いがあります。

onMouseMove

マウスが要素上で動いたときに発火します。頻繁に呼ばれるので注意が必要ですね。

<div onMouseMove={(e) => console.log(`位置: ${e.clientX}, ${e.clientY}`)}>
  マウスを動かしてみて
</div>

マウスボタン系イベント

onMouseDown

マウスボタンを押した瞬間に発火します。

<button onMouseDown={() => console.log('押した')}>
  押してみて
</button>

onMouseUp

マウスボタンを離した瞬間に発火します。

<button onMouseUp={() => console.log('離した')}>
  離してみて
</button>

実例:シンプルなクリックカウンター

実際に動くコードで理解を深めましょう。

基本的なクリックカウンター

import { useState } from 'react';

function ClickCounter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>クリック回数: {count}</p>
      <button onClick={handleClick}>
        クリックしてね
      </button>
    </div>
  );
}

ポイント:

  • useStateで状態を管理
  • handleClick関数でカウントを増やす
  • onClickでイベントハンドラを設定

説明

useState:

Reactで状態(変化する値)を管理するための機能です。countが現在の値、setCountが値を変更する関数ですね。

イベントハンドラ:

handleClickのような、イベントが発生したときに実行される関数のことです。


イベントオブジェクトの活用

イベントハンドラには、自動的にイベントオブジェクトが渡されます。

イベントオブジェクトとは

マウスイベントの詳細情報を持つオブジェクトです。

function handleClick(event) {
  console.log('クリックされた位置:', event.clientX, event.clientY);
  console.log('押されたボタン:', event.button);
}

<button onClick={handleClick}>クリック</button>

主要なプロパティ

マウス座標:

プロパティ説明
clientXビューポート(表示領域)内のX座標
clientYビューポート内のY座標
pageXページ全体でのX座標
pageYページ全体でのY座標
screenXスクリーン全体でのX座標
screenYスクリーン全体でのY座標

ボタン情報:

プロパティ説明
button押されたボタン(0=左, 1=中, 2=右)
buttons現在押されているボタンの組み合わせ

修飾キー:

プロパティ説明
ctrlKeyCtrlキーが押されているか
shiftKeyShiftキーが押されているか
altKeyAltキーが押されているか
metaKeyCmd(Mac)やWin(Windows)キーが押されているか

実例:クリック位置を表示

import { useState } from 'react';

function ClickPosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const handleClick = (e) => {
    setPosition({
      x: e.clientX,
      y: e.clientY
    });
  };

  return (
    <div 
      onClick={handleClick}
      style={{ 
        height: '300px', 
        border: '1px solid black',
        cursor: 'crosshair'
      }}
    >
      <p>クリックした位置: ({position.x}, {position.y})</p>
      <p>どこでもクリックしてみてください</p>
    </div>
  );
}

ホバーエフェクトの実装

マウスを乗せると色が変わる、よく見る動作を作ってみましょう。

基本的なホバーエフェクト

import { useState } from 'react';

function HoverButton() {
  const [isHovered, setIsHovered] = useState(false);

  return (
    <button
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
      style={{
        backgroundColor: isHovered ? '#4CAF50' : '#ddd',
        color: isHovered ? 'white' : 'black',
        padding: '10px 20px',
        border: 'none',
        borderRadius: '5px',
        cursor: 'pointer',
        transition: 'all 0.3s'
      }}
    >
      ホバーしてみて
    </button>
  );
}

仕組み:

  1. isHoveredという状態を用意
  2. マウスが入ったらtrueに設定
  3. マウスが出たらfalseに設定
  4. 状態に応じてスタイルを変更

複数の要素のホバー管理

import { useState } from 'react';

function HoverList() {
  const [hoveredId, setHoveredId] = useState(null);

  const items = [
    { id: 1, name: 'アイテム1' },
    { id: 2, name: 'アイテム2' },
    { id: 3, name: 'アイテム3' }
  ];

  return (
    <ul style={{ listStyle: 'none', padding: 0 }}>
      {items.map(item => (
        <li
          key={item.id}
          onMouseEnter={() => setHoveredId(item.id)}
          onMouseLeave={() => setHoveredId(null)}
          style={{
            padding: '10px',
            backgroundColor: hoveredId === item.id ? '#e3f2fd' : 'white',
            border: '1px solid #ccc',
            marginBottom: '5px',
            cursor: 'pointer',
            transition: 'background-color 0.2s'
          }}
        >
          {item.name}
        </li>
      ))}
    </ul>
  );
}

ドラッグ&ドロップの実装

要素をドラッグできるようにしてみましょう。

シンプルなドラッグ可能な要素

import { useState } from 'react';

function DraggableBox() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [isDragging, setIsDragging] = useState(false);
  const [dragStart, setDragStart] = useState({ x: 0, y: 0 });

  const handleMouseDown = (e) => {
    setIsDragging(true);
    setDragStart({
      x: e.clientX - position.x,
      y: e.clientY - position.y
    });
  };

  const handleMouseMove = (e) => {
    if (!isDragging) return;

    setPosition({
      x: e.clientX - dragStart.x,
      y: e.clientY - dragStart.y
    });
  };

  const handleMouseUp = () => {
    setIsDragging(false);
  };

  return (
    <div
      style={{ height: '400px', position: 'relative', border: '1px solid #ccc' }}
      onMouseMove={handleMouseMove}
      onMouseUp={handleMouseUp}
    >
      <div
        onMouseDown={handleMouseDown}
        style={{
          position: 'absolute',
          left: `${position.x}px`,
          top: `${position.y}px`,
          width: '100px',
          height: '100px',
          backgroundColor: isDragging ? '#4CAF50' : '#2196F3',
          color: 'white',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          cursor: isDragging ? 'grabbing' : 'grab',
          userSelect: 'none'
        }}
      >
        ドラッグしてね
      </div>
    </div>
  );
}

仕組み:

  1. onMouseDownでドラッグ開始
  2. onMouseMoveで位置を更新
  3. onMouseUpでドラッグ終了

合成イベント(SyntheticEvent)の理解

Reactでは、ブラウザのネイティブイベントを合成イベントでラップしています。

合成イベントとは

クロスブラウザ対応を自動化:

ブラウザによってイベントの挙動が異なることがありますが、Reactの合成イベントはすべてのブラウザで同じように動作します。

メモリ効率が良い:

イベントオブジェクトは再利用されるため、メモリ効率が良いんですよ。

ネイティブイベントへのアクセス

もし本当のネイティブイベントが必要な場合は、nativeEventプロパティを使います。

function handleClick(event) {
  console.log('合成イベント:', event);
  console.log('ネイティブイベント:', event.nativeEvent);
}

イベントの伝播制御

イベントは、子要素から親要素へと伝播していきます。

イベントバブリング

function Parent() {
  return (
    <div onClick={() => console.log('親がクリックされた')}>
      親要素
      <button onClick={() => console.log('子がクリックされた')}>
        子要素
      </button>
    </div>
  );
}

ボタンをクリックすると、両方のログが出力されます。

出力順:

  1. 子がクリックされた
  2. 親がクリックされた

伝播を止める

stopPropagation:

親要素への伝播を止めたい場合に使います。

function Parent() {
  return (
    <div onClick={() => console.log('親がクリックされた')}>
      親要素
      <button onClick={(e) => {
        e.stopPropagation();
        console.log('子がクリックされた');
      }}>
        子要素(伝播しない)
      </button>
    </div>
  );
}

これで、ボタンをクリックしても親のイベントは発火しません。

デフォルト動作を防ぐ

preventDefault:

リンクのクリックやフォーム送信などのデフォルト動作を防ぎます。

function CustomLink() {
  const handleClick = (e) => {
    e.preventDefault();
    console.log('リンクがクリックされたけど、遷移しない');
  };

  return (
    <a href="https://example.com" onClick={handleClick}>
      クリックしても遷移しないリンク
    </a>
  );
}

パフォーマンス最適化

マウスイベントは頻繁に発火するため、パフォーマンスに注意が必要です。

useCallbackでメモ化

イベントハンドラをメモ化することで、不要な再レンダリングを防げます。

import { useState, useCallback } from 'react';

function OptimizedComponent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []); // 依存配列が空なので、関数は再生成されない

  return (
    <button onClick={handleClick}>
      クリック回数: {count}
    </button>
  );
}

スロットリング(間引き)

onMouseMoveのような頻繁に発火するイベントでは、スロットリングが有効です。

import { useState, useRef } from 'react';

function ThrottledMouseMove() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const throttleRef = useRef(false);

  const handleMouseMove = (e) => {
    if (throttleRef.current) return;

    throttleRef.current = true;
    setTimeout(() => {
      throttleRef.current = false;
    }, 100); // 100ms間隔で実行

    setPosition({ x: e.clientX, y: e.clientY });
  };

  return (
    <div 
      onMouseMove={handleMouseMove}
      style={{ height: '300px', border: '1px solid black' }}
    >
      <p>マウス位置: ({position.x}, {position.y})</p>
    </div>
  );
}

デバウンス(遅延実行)

入力が止まってから処理を実行したい場合は、デバウンスを使います。

import { useState, useEffect } from 'react';

function DebouncedHover() {
  const [isHovered, setIsHovered] = useState(false);
  const [showTooltip, setShowTooltip] = useState(false);

  useEffect(() => {
    let timer;
    if (isHovered) {
      timer = setTimeout(() => {
        setShowTooltip(true);
      }, 500); // 500ms後に表示
    } else {
      setShowTooltip(false);
    }

    return () => clearTimeout(timer);
  }, [isHovered]);

  return (
    <div
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
      style={{ position: 'relative', padding: '10px' }}
    >
      ホバーしてください
      {showTooltip && (
        <div style={{
          position: 'absolute',
          background: 'black',
          color: 'white',
          padding: '5px',
          borderRadius: '3px',
          top: '100%',
          left: '0'
        }}>
          ツールチップが表示されました
        </div>
      )}
    </div>
  );
}

TypeScriptでの型定義

TypeScriptを使う場合、イベントの型を正しく指定しましょう。

基本的な型指定

import { MouseEvent } from 'react';

function TypedButton() {
  const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
    console.log('ボタンがクリックされた');
  };

  return <button onClick={handleClick}>クリック</button>;
}

主要な型

MouseEvent:

MouseEvent<HTMLElement>
MouseEvent<HTMLButtonElement>
MouseEvent<HTMLDivElement>
MouseEvent<HTMLAnchorElement>

要素の種類に応じて、型パラメータを変えます。

イベントハンドラの型

type ClickHandler = (event: MouseEvent<HTMLButtonElement>) => void;

function Component() {
  const handleClick: ClickHandler = (event) => {
    console.log('型安全なクリックハンドラ');
  };

  return <button onClick={handleClick}>クリック</button>;
}

カスタムフックでの型定義

import { useState, MouseEvent } from 'react';

function useHover() {
  const [isHovered, setIsHovered] = useState(false);

  const handlers = {
    onMouseEnter: (e: MouseEvent<HTMLElement>) => setIsHovered(true),
    onMouseLeave: (e: MouseEvent<HTMLElement>) => setIsHovered(false)
  };

  return [isHovered, handlers] as const;
}

function HoverComponent() {
  const [isHovered, hoverHandlers] = useHover();

  return (
    <div {...hoverHandlers}>
      {isHovered ? 'ホバー中' : 'ホバーしてね'}
    </div>
  );
}

実用的なパターン集

よく使われるパターンをまとめました。

パターン1:長押し検出

import { useState, useRef } from 'react';

function LongPressButton() {
  const [isLongPress, setIsLongPress] = useState(false);
  const timerRef = useRef(null);

  const handleMouseDown = () => {
    timerRef.current = setTimeout(() => {
      setIsLongPress(true);
      console.log('長押しされた');
    }, 1000); // 1秒間押し続けたら発火
  };

  const handleMouseUp = () => {
    clearTimeout(timerRef.current);
    setIsLongPress(false);
  };

  return (
    <button
      onMouseDown={handleMouseDown}
      onMouseUp={handleMouseUp}
      onMouseLeave={handleMouseUp}
      style={{
        padding: '20px',
        backgroundColor: isLongPress ? '#4CAF50' : '#2196F3',
        color: 'white',
        border: 'none',
        borderRadius: '5px'
      }}
    >
      1秒間押し続けてね
    </button>
  );
}

パターン2:カスタムコンテキストメニュー

import { useState } from 'react';

function CustomContextMenu() {
  const [menuPosition, setMenuPosition] = useState(null);

  const handleContextMenu = (e) => {
    e.preventDefault();
    setMenuPosition({ x: e.clientX, y: e.clientY });
  };

  const handleClick = () => {
    setMenuPosition(null); // メニューを閉じる
  };

  return (
    <>
      <div
        onContextMenu={handleContextMenu}
        onClick={handleClick}
        style={{
          height: '300px',
          border: '1px solid #ccc',
          position: 'relative'
        }}
      >
        右クリックしてメニューを表示

        {menuPosition && (
          <div
            style={{
              position: 'fixed',
              left: menuPosition.x,
              top: menuPosition.y,
              backgroundColor: 'white',
              border: '1px solid #ccc',
              boxShadow: '0 2px 5px rgba(0,0,0,0.2)',
              padding: '5px 0',
              zIndex: 1000
            }}
          >
            <div style={{ padding: '5px 20px', cursor: 'pointer' }}>
              オプション1
            </div>
            <div style={{ padding: '5px 20px', cursor: 'pointer' }}>
              オプション2
            </div>
            <div style={{ padding: '5px 20px', cursor: 'pointer' }}>
              オプション3
            </div>
          </div>
        )}
      </div>
    </>
  );
}

パターン3:マウスフォロー要素

import { useState, useEffect } from 'react';

function MouseFollower() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };

    window.addEventListener('mousemove', handleMouseMove);

    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []);

  return (
    <div
      style={{
        position: 'fixed',
        left: position.x,
        top: position.y,
        width: '20px',
        height: '20px',
        backgroundColor: '#4CAF50',
        borderRadius: '50%',
        pointerEvents: 'none',
        transform: 'translate(-50%, -50%)',
        transition: 'all 0.1s',
        zIndex: 9999
      }}
    />
  );
}

よくあるトラブルと解決法

マウスイベントでよくある問題を見てみましょう。

問題1:イベントが発火しない

原因:

親要素にイベントを設定しているが、子要素でstopPropagationされている可能性があります。

解決策:

// 問題のあるコード
<div onClick={() => console.log('親')}>
  <button onClick={(e) => e.stopPropagation()}>
    クリックしても親に伝わらない
  </button>
</div>

// 解決策: キャプチャフェーズで捕捉
<div onClickCapture={() => console.log('親(キャプチャ)')}>
  <button onClick={(e) => e.stopPropagation()}>
    これなら親も反応する
  </button>
</div>

問題2:ホバー時のちらつき

原因:

子要素にマウスが入ると、親のonMouseLeaveが発火してしまう。

解決策:

onMouseOver/onMouseOutの代わりにonMouseEnter/onMouseLeaveを使いましょう。

// 良い例
<div
  onMouseEnter={() => setHovered(true)}
  onMouseLeave={() => setHovered(false)}
>
  <span>子要素があってもOK</span>
</div>

問題3:ドラッグ中にテキストが選択される

原因:

ドラッグ中にテキスト選択が起きてしまう。

解決策:

<div
  onMouseDown={handleMouseDown}
  style={{
    userSelect: 'none',  // テキスト選択を無効化
    WebkitUserSelect: 'none',
    MozUserSelect: 'none'
  }}
>
  ドラッグ可能な要素
</div>

問題4:モバイルで動作しない

原因:

マウスイベントは、タッチデバイスでは動作が異なります。

解決策:

タッチイベントも併用しましょう。

function TouchAndMouseButton() {
  const [isPressed, setIsPressed] = useState(false);

  const handleStart = () => setIsPressed(true);
  const handleEnd = () => setIsPressed(false);

  return (
    <button
      onMouseDown={handleStart}
      onMouseUp={handleEnd}
      onTouchStart={handleStart}
      onTouchEnd={handleEnd}
      style={{
        backgroundColor: isPressed ? '#4CAF50' : '#ddd',
        padding: '20px'
      }}
    >
      押してみて
    </button>
  );
}

よくある質問

Q: onClickとonMouseDownの違いは?

A: onClickはクリック動作全体(押して離す)で発火しますが、onMouseDownはボタンを押した瞬間だけで発火します。ドラッグ操作を実装する場合はonMouseDownを使いますね。

Q: イベントハンドラはインラインで書くべき?それとも別関数に?

A: 簡単な処理ならインラインでもOKですが、複雑な処理や再利用する場合は別関数に切り出すことをおすすめします。パフォーマンス最適化の観点からも、useCallbackと組み合わせて別関数にする方が良いですよ。

Q: 合成イベントとネイティブイベントの違いは?

A: React の合成イベントはブラウザの違いを吸収してくれるラッパーです。基本的には合成イベントを使えば問題ありませんが、特殊な操作が必要な場合のみevent.nativeEventでネイティブイベントにアクセスします。

Q: マウスイベントとタッチイベント、どちらを使うべき?

A: デスクトップ専用ならマウスイベントでOKですが、モバイル対応が必要なら両方をサポートするか、ポインターイベント(onPointerDownなど)を使うことをおすすめします。

Q: preventDefaultとstopPropagationの使い分けは?

A: preventDefaultはブラウザのデフォルト動作(リンク遷移、フォーム送信など)を防ぎ、stopPropagationはイベントの親要素への伝播を止めます。目的に応じて使い分けましょう。


まとめ:マウスイベントを使いこなそう

Reactのマウスイベントについて、重要なポイントをおさらいします。

今日学んだこと:

  • Reactではキャメルケース(onClick)でイベントを書く
  • 主要イベント:onClick、onMouseEnter、onMouseLeave、onMouseMove
  • イベントオブジェクトで座標や修飾キーを取得できる
  • ホバーエフェクトは状態管理で実装
  • ドラッグはonMouseDown/Move/Upの組み合わせ
  • 合成イベントがクロスブラウザ対応を担当
  • preventDefault/stopPropagationで動作を制御
  • useCallbackやスロットリングでパフォーマンス最適化
  • TypeScriptでは適切な型指定が重要

マウスイベントは、インタラクティブなUIを作る上で欠かせない機能です。

最初は種類が多くて混乱するかもしれませんが、実際に手を動かして試してみることが一番の近道ですよ。クリック、ホバー、ドラッグと、徐々に複雑な操作にチャレンジしていけば、自然と使いこなせるようになります。

ユーザーにとって快適で直感的なインターフェースを作るために、ぜひ今日学んだ知識を活かしてみてくださいね!


関連記事:

  • Reactのイベントハンドリング完全ガイド
  • useStateとuseEffectの使い方
  • Reactのパフォーマンス最適化テクニック

コメント

タイトルとURLをコピーしました