LaravelとReactで実装するSPAメッセージ機能:リロードなしでUXを向上させる方法

はじめに

Webアプリケーションにおいて、ユーザー体験(UX)は非常に重要な要素です。特にチャットやメッセージ機能では、ページのリロードが発生するとユーザーの操作が中断され、スムーズな会話体験が損なわれてしまいます。今回は、Laravel(バックエンド)とReact(フロントエンド)を使用して、ページをリロードせずにメッセージを送信できるSPA(Single Page Application)形式の実装方法について解説します。

従来の実装とSPA実装の違い

従来の実装(ページリロードあり)

従来の実装では、フォーム送信時に以下のような流れになります。

  1. ユーザーがメッセージを入力して送信ボタンをクリック
  2. フォームがサーバーにPOSTリクエストを送信
  3. サーバーがデータを処理し、新しいページをレンダリング
  4. ブラウザが完全に画面をリロード
  5. ユーザーは入力位置や画面の状態を失う

SPA実装(リロードなし)

SPA実装では、フォーム送信時に以下のような流れになります。

  1. ユーザーがメッセージを入力して送信ボタンをクリック
  2. JavaScriptがAPIリクエストをサーバーに送信
  3. サーバーがデータを処理し、JSONレスポンスを返す
  4. フロントエンドがレスポンスを受け取り、UIを更新
  5. ユーザーの操作が中断されない

実装方法

今回は、LaravelとReact(TypeScript)を使用したSPAメッセージ機能の実装方法を紹介します。

バックエンド(Laravel)の実装

まず、メッセージ送信を処理するコントローラーを修正します。従来のリダイレクト処理に加えて、JSONレスポンスを返すようにします。

public function store(Request $request, ConversationGroup $conversationGroup): JsonResponse|RedirectResponse
{
    // バリデーション
    $request->validate([
        'message' => 'required|string|max:1000',
    ]);

    // ユーザー権限チェック
    if (!$conversationGroup->hasParticipant(Auth::id())) {
        abort(403, 'Unauthorized action.');
    }

    // メッセージ作成
    $message = new DirectMessage([
        'sender_id' => Auth::id(),
        'message' => $request->message,
    ]);

    $conversationGroup->messages()->save($message);
    $conversationGroup->touch(); // updated_atを更新

    // Ajax リクエストの場合は JSON レスポンスを返す
    if ($request->expectsJson() || $request->ajax()) {
        return response()->json([
            'success' => true,
            'message' => $message->load('sender'),
        ]);
    }

    // 通常のフォーム送信の場合はリダイレクト
    return redirect()->back();
}

このコントローラーのポイントは以下の通りです。

  • JsonResponse|RedirectResponseで両方のレスポンスタイプをサポート
  • リクエストタイプに応じて適切なレスポンスを返す
  • APIリクエストの場合は送信者情報も含めて返す

フロントエンド(React/TypeScript)の実装

次に、React側でのメッセージ送信処理を実装します。

import { useState, useEffect, useRef } from "react";
import { useForm } from "@inertiajs/react";
import axios from "axios";
import DirectMessage, { DirectMessageType } from "@/Components/DirectMessage";

// コンポーネント内
export default function Show({
    auth,
    conversationGroup,
    messages: initialMessages,
    participants,
}) {
    // メッセージの状態管理
    const [messages, setMessages] = useState<DirectMessageType[]>(initialMessages);
    const [sending, setSending] = useState(false);
    const messagesContainerRef = useRef<HTMLDivElement>(null);

    const { data, setData, errors, reset } = useForm({
        message: "",
    });

    // メッセージ送信処理
    const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        
        if (!data.message.trim() || sending) {
            return;
        }
        
        setSending(true);
        
        try {
            // CSRFトークンの取得
            const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
            
            // APIでメッセージを送信
            const response = await axios.post(
                route("messages.store", conversationGroup.id), 
                data,
                {
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRF-TOKEN': csrfToken
                    }
                }
            );
            
            // 成功したら新しいメッセージを追加
            if (response.data && response.data.message) {
                const newMessage: DirectMessageType = {
                    ...response.data.message,
                    sender: auth.user
                };
                
                setMessages([...messages, newMessage]);
                reset("message");
            }
        } catch (error) {
            console.error('メッセージ送信エラー:', error);
        } finally {
            setSending(false);
        }
    };

    // メッセージが追加されたらスクロール位置を最下部に移動
    useEffect(() => {
        if (messagesContainerRef.current) {
            messagesContainerRef.current.scrollTop =
                messagesContainerRef.current.scrollHeight;
        }
    }, [messages]);

    // JSXの返却(省略)
}

フロントエンド実装のポイントは以下の通りです。

  1. 状態管理
  • useStateでメッセージリストと送信中の状態を管理
  • 初期データはpropsから取得
  1. 非同期処理
  • async/awaitを使った非同期処理
  • 送信中の状態管理(二重送信防止)
  • エラーハンドリング
  1. UX向上
  • 新しいメッセージ送信後に自動的に入力欄をリセット
  • 新しいメッセージ追加時に自動スクロール
  • 送信中の視覚的フィードバック

実装のメリット

このSPA実装には以下のようなメリットがあります。

  1. シームレスなユーザー体験
  • ページがリロードされないため、会話のコンテキストが維持される
  • スムーズな操作感でユーザーのフラストレーションが軽減
  1. レスポンシブな操作感
  • 送信後すぐにUIが更新されるため、操作への即時フィードバックがある
  • 読み込み中やエラー状態も視覚的に表現できる
  1. サーバー負荷の軽減
  • 必要なデータのみを送受信するため、帯域幅の使用が最適化される
  • 全ページを再読み込みする必要がないため、サーバーリソースの使用が効率化
  1. 拡張性
  • この基本的なパターンを応用して、リアルタイム通知やタイピングインジケーターなどの機能を追加可能

注意点とさらなる改善点

  1. エラーハンドリング
  • ネットワークエラーや認証エラーなど、様々なエラーケースに対応する必要がある
  • ユーザーにわかりやすいエラーメッセージを表示する
  1. バリデーション
  • フロントエンドでも入力値のバリデーションを行うことでUXを向上させられる
  1. リアルタイム通信
  • WebSocketやPusherなどを活用して、真のリアルタイムチャットにアップグレードできる
  1. オフライン対応
  • Service Workerを使用してオフライン時のメッセージ送信をキューに入れるなどの機能も可能

まとめ

今回は、LaravelとReactを使ったSPA形式のメッセージ送信機能の実装方法を紹介しました。ページをリロードせずにメッセージを送信することで、ユーザー体験が大幅に向上します。この実装パターンは、チャット機能だけでなく、コメント機能やフォーム送信など、様々な場面で応用可能です。ユーザー体験を重視したウェブアプリケーション開発の参考にしていただければ幸いです。最後に、SPAのアプローチはモダンなWeb開発における標準的な手法になりつつあります。ユーザーの期待も高まる中、このような実装を取り入れることは、現代のWebアプリケーション開発において非常に重要なスキルとなっています。