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フロントエンドと連携させる方法を解説しました。この実装により、以下のことが可能になります:
- Laravelバックエンドでの通知の作成と保存
- WebSocketを使用したリアルタイム通知の配信
- Reactでの通知の表示とインタラクティブな操作
- 既読/未読状態の管理
この基本的な設定に加えて、プロジェクトの要件に応じて機能を拡張することができます。通知は現代のWebアプリケーションにおいて重要なユーザーエンゲージメント機能の一つであり、適切に実装することでユーザー体験を大幅に向上させることができます。
 
  
  
  
  
コメント