Laravelの通知機能をReactに連携させる

Laravelの洗練された通知システムとReactフロントエンドを組み合わせることで、リアルタイムでユーザーフレンドリーな通知体験を実現できます。この記事では、Laravelバックエンドで生成された通知をReactフロントエンドで表示する方法について解説します。

はじめに

モダンなWebアプリケーションでは、ユーザーに対してリアルタイムの通知を提供することが重要です。Laravelには強力な通知システムが組み込まれていますが、Reactのようなフロントエンドフレームワークと連携させることで、さらに魅力的なユーザー体験を実現できます。

前提条件

  • Laravel 8.x 以上
  • React環境(create-react-appなど)
  • Laravel Sanctum(API認証用)
  • Laravel Echo + Pusher(リアルタイム通信用)

実装手順

1. Laravel側の設定

通知クラスの作成

まず、Laravelで通知クラスを作成します。

php artisan make:notification NewCommentNotification

app/Notifications/NewCommentNotification.phpを編集します:

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;

class NewCommentNotification extends Notification implements ShouldQueue
{
    use Queueable;

    protected $comment;

    public function __construct($comment)
    {
        $this->comment = $comment;
    }

    public function via($notifiable)
    {
        return ['database', 'broadcast'];
    }

    public function toDatabase($notifiable)
    {
        return [
            'comment_id' => $this->comment->id,
            'post_id' => $this->comment->post_id,
            'user_id' => $this->comment->user_id,
            'user_name' => $this->comment->user->name,
            'message' => substr($this->comment->body, 0, 50),
        ];
    }

    public function toBroadcast($notifiable)
    {
        return new BroadcastMessage($this->toDatabase($notifiable));
    }
}

データベース通知の設定

通知をデータベースに保存できるようにマイグレーションを実行します:

php artisan notifications:table
php artisan migrate

ブロードキャスト設定

.envファイルでPusherの設定を行います:

BROADCAST_DRIVER=pusher
PUSHER_APP_ID=your_app_id
PUSHER_APP_KEY=your_app_key
PUSHER_APP_SECRET=your_app_secret
PUSHER_APP_CLUSTER=your_app_cluster

config/broadcasting.phpでPusherの設定が正しいことを確認します。

通知の送信

コメントが作成されたときに通知を送信する例:

// CommentController.php
public function store(Request $request)
{
    // コメントの保存ロジック
    $comment = Comment::create([
        'user_id' => auth()->id(),
        'post_id' => $request->post_id,
        'body' => $request->body
    ]);
    
    // 投稿者に通知を送信
    $post = Post::find($request->post_id);
    $post->user->notify(new NewCommentNotification($comment));
    
    return response()->json($comment);
}

APIルートの作成

通知を取得するためのAPIルートを作成します:

// routes/api.php
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/notifications', function () {
        return auth()->user()->notifications;
    });
    
    Route::patch('/notifications/{id}', function ($id) {
        auth()->user()->notifications->where('id', $id)->markAsRead();
        return response()->noContent();
    });
});

2. React側の設定

必要なパッケージのインストール

npm install laravel-echo pusher-js axios

Echo設定

src/bootstrap.jsファイルを作成します:

import Echo from 'laravel-echo';
import Pusher from 'pusher-js';

window.Pusher = Pusher;

window.Echo = new Echo({
  broadcaster: 'pusher',
  key: process.env.REACT_APP_PUSHER_APP_KEY,
  cluster: process.env.REACT_APP_PUSHER_APP_CLUSTER,
  forceTLS: true,
  authorizer: (channel, options) => {
    return {
      authorize: (socketId, callback) => {
        axios.post('/api/broadcasting/auth', {
          socket_id: socketId,
          channel_name: channel.name
        })
          .then(response => {
            callback(false, response.data);
          })
          .catch(error => {
            callback(true, error);
          });
      }
    };
  }
});

.env設定

Reactの.envファイルに以下を追加します:

REACT_APP_PUSHER_APP_KEY=your_app_key
REACT_APP_PUSHER_APP_CLUSTER=your_app_cluster

通知コンポーネントの作成

// src/components/Notifications.jsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import './bootstrap';

const Notifications = () => {
  const [notifications, setNotifications] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 初期通知の読み込み
    const fetchNotifications = async () => {
      try {
        const response = await axios.get('/api/notifications');
        setNotifications(response.data);
        setLoading(false);
      } catch (error) {
        console.error('通知の取得に失敗しました', error);
        setLoading(false);
      }
    };

    fetchNotifications();

    // リアルタイム通知のリスニング
    const userId = localStorage.getItem('userId'); // ユーザーIDの取得方法はアプリによって異なる
    
    if (userId) {
      window.Echo.private(`App.Models.User.${userId}`)
        .notification((notification) => {
          setNotifications(prevNotifications => [notification, ...prevNotifications]);
        });
    }

    // クリーンアップ
    return () => {
      if (userId) {
        window.Echo.leave(`App.Models.User.${userId}`);
      }
    };
  }, []);

  const markAsRead = async (id) => {
    try {
      await axios.patch(`/api/notifications/${id}`);
      setNotifications(notifications.map(notification => {
        if (notification.id === id) {
          return { ...notification, read_at: new Date() };
        }
        return notification;
      }));
    } catch (error) {
      console.error('既読の設定に失敗しました', error);
    }
  };

  if (loading) {
    return <div>通知を読み込み中...</div>;
  }

  return (
    <div className="notifications-container">
      <h2>通知</h2>
      {notifications.length === 0 ? (
        <p>新しい通知はありません</p>
      ) : (
        <ul className="notification-list">
          {notifications.map(notification => (
            <li
              key={notification.id}
              className={`notification-item ${!notification.read_at ? 'unread' : ''}`}
              onClick={() => markAsRead(notification.id)}
            >
              <div className="notification-content">
                <p>{notification.data.user_name}さんがあなたの投稿にコメントしました: "{notification.data.message}..."</p>
                <small>{new Date(notification.created_at).toLocaleString()}</small>
              </div>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

export default Notifications;

スタイルの追加

通知コンポーネント用のCSSを作成します:

/* src/components/Notifications.css */
.notifications-container {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.notification-list {
  list-style: none;
  padding: 0;
}

.notification-item {
  padding: 15px;
  border-bottom: 1px solid #eee;
  cursor: pointer;
  transition: background-color 0.2s;
}

.notification-item:hover {
  background-color: #f9f9f9;
}

.notification-item.unread {
  background-color: #f0f7ff;
  font-weight: bold;
}

.notification-item.unread:hover {
  background-color: #e6f0ff;
}

.notification-content p {
  margin: 0 0 5px;
}

.notification-content small {
  color: #777;
}

3. 通知アイコンとドロップダウンの実装

ナビゲーションバーに通知アイコンとドロップダウンを追加します:

// src/components/NotificationIcon.jsx
import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import './bootstrap';

const NotificationIcon = () => {
  const [notifications, setNotifications] = useState([]);
  const [unreadCount, setUnreadCount] = useState(0);
  const [isOpen, setIsOpen] = useState(false);
  const dropdownRef = useRef(null);

  useEffect(() => {
    // 初期通知の読み込み
    const fetchNotifications = async () => {
      try {
        const response = await axios.get('/api/notifications');
        setNotifications(response.data);
        setUnreadCount(response.data.filter(n => !n.read_at).length);
      } catch (error) {
        console.error('通知の取得に失敗しました', error);
      }
    };

    fetchNotifications();

    // リアルタイム通知のリスニング
    const userId = localStorage.getItem('userId');
    
    if (userId) {
      window.Echo.private(`App.Models.User.${userId}`)
        .notification((notification) => {
          setNotifications(prevNotifications => [notification, ...prevNotifications]);
          setUnreadCount(prevCount => prevCount + 1);
        });
    }

    // クリックイベントの処理(ドロップダウン外クリックで閉じる)
    const handleClickOutside = (event) => {
      if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
        setIsOpen(false);
      }
    };

    document.addEventListener('mousedown', handleClickOutside);

    // クリーンアップ
    return () => {
      if (userId) {
        window.Echo.leave(`App.Models.User.${userId}`);
      }
      document.removeEventListener('mousedown', handleClickOutside);
    };
  }, []);

  const toggleDropdown = () => {
    setIsOpen(!isOpen);
  };

  const markAsRead = async (id) => {
    try {
      await axios.patch(`/api/notifications/${id}`);
      setNotifications(notifications.map(notification => {
        if (notification.id === id) {
          return { ...notification, read_at: new Date() };
        }
        return notification;
      }));
      setUnreadCount(prevCount => Math.max(0, prevCount - 1));
    } catch (error) {
      console.error('既読の設定に失敗しました', error);
    }
  };

  return (
    <div className="notification-icon-container" ref={dropdownRef}>
      <div className="notification-icon" onClick={toggleDropdown}>
        <i className="fa fa-bell"></i>
        {unreadCount > 0 && <span className="notification-badge">{unreadCount}</span>}
      </div>
      
      {isOpen && (
        <div className="notification-dropdown">
          <h3>通知</h3>
          {notifications.length === 0 ? (
            <p className="no-notifications">新しい通知はありません</p>
          ) : (
            <ul>
              {notifications.slice(0, 5).map(notification => (
                <li
                  key={notification.id}
                  className={`notification-item ${!notification.read_at ? 'unread' : ''}`}
                  onClick={() => markAsRead(notification.id)}
                >
                  <p>{notification.data.user_name}さんがコメントしました</p>
                  <small>{new Date(notification.created_at).toLocaleString()}</small>
                </li>
              ))}
              {notifications.length > 5 && (
                <li className="view-all">
                  <a href="/notifications">すべての通知を見る</a>
                </li>
              )}
            </ul>
          )}
        </div>
      )}
    </div>
  );
};

export default NotificationIcon;

ナビゲーションバーへの追加

// src/components/Navbar.jsx
import React from 'react';
import { Link } from 'react-router-dom';
import NotificationIcon from './NotificationIcon';

const Navbar = () => {
  return (
    <nav className="navbar">
      <div className="navbar-brand">
        <Link to="/">MyAppName</Link>
      </div>
      <div className="navbar-menu">
        <Link to="/dashboard">ダッシュボード</Link>
        <Link to="/posts">投稿一覧</Link>
        <NotificationIcon />
        <div className="user-menu">
          {/* ユーザーメニュー */}
        </div>
      </div>
    </nav>
  );
};

export default Navbar;

発展的な機能

1. 通知設定ページの実装

ユーザーが通知の種類ごとに受信設定を変更できるようにします。

2. プッシュ通知の追加

ブラウザのプッシュ通知APIを使用して、アプリがバックグラウンドにあるときでも通知を表示できます。

3. 通知の既読一括処理

すべての通知を一括で既読にするボタンを追加します。

パフォーマンスの最適化

ページネーション

通知が多い場合は、ページネーションを実装してパフォーマンスを向上させます:

// routes/api.php
Route::get('/notifications', function () {
    return auth()->user()->notifications()->paginate(10);
});
// Reactコンポーネントでページネーションを処理
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);

const loadMore = async () => {
  setPage(prevPage => prevPage + 1);
  try {
    const response = await axios.get(`/api/notifications?page=${page + 1}`);
    if (response.data.data.length > 0) {
      setNotifications([...notifications, ...response.data.data]);
    } else {
      setHasMore(false);
    }
  } catch (error) {
    console.error('通知の読み込みに失敗しました', error);
  }
};

まとめ

この記事では、Laravelの通知システムをReactフロントエンドと連携させる方法を解説しました。この実装により、以下のことが可能になります:

  1. Laravelバックエンドでの通知の作成と保存
  2. WebSocketを使用したリアルタイム通知の配信
  3. Reactでの通知の表示とインタラクティブな操作
  4. 既読/未読状態の管理

この基本的な設定に加えて、プロジェクトの要件に応じて機能を拡張することができます。通知は現代のWebアプリケーションにおいて重要なユーザーエンゲージメント機能の一つであり、適切に実装することでユーザー体験を大幅に向上させることができます。

コメント

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