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アプリケーションにおいて重要なユーザーエンゲージメント機能の一つであり、適切に実装することでユーザー体験を大幅に向上させることができます。
コメント