ReactとLaravel連携してデータ更新するまでの流れ

# サンプル
今回は、編集対象イベント情報を更新するまでのサンプルを作っていこうと思います。
<b>【手順】</b>
<b>一覧画面から更新対象のイベントを選択して更新するボタンをクリックします。</b>
![一覧画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/41e2901d-eef9-b566-3580-b07bbdef22b9.png)

<b>編集画面に遷移します</b>
![編集画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/9c1d74dc-34d9-7c1b-c38c-602eb15c0b16.png)

<b>更新内容を入力します</b>
![編集画面_更新.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/98aa80d7-9e03-0f38-913c-7db2562ef423.png)

<b>更新内容が無事データベースに登録されると、メッセージが表示されます。</b>
![編集画面_更新完了.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/da5d0978-8edf-5504-3102-0daf0cadf4e4.png)

<b>メッセージの`OKボタン`をクリックすると自動的に一覧画面に遷移します</b>
![更新後の一覧画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/cad4220a-3dc0-76b5-a1b9-fdc3cd004f1f.png)


# このアプリの関連記事

https://qiita.com/SKY-HaYaTo/items/5d69a85bf6620e0ec78c

https://qiita.com/SKY-HaYaTo/items/3edf7ca4f38516e0e70e

# ディレクトリ構造

```
ReactLaravelProject
├─ backend
│  ├─ app
│  │  ├─ Http
│  │  │  └─ Controllers
│  │  │     ├─ EventController.php
│  │  │     └─ RegisterController.php
│  │  └─ Models
│  │     └─ Event.php
│  └─ Routes
│     └─ api.php
└─ frontend
      └─ src
         ├─ components
         │  ├─ card
         │  │  └─ page.tsx
         │  └─ navbar
         │     └─ page.tsx
         ├─ dashboard  
         │     └─ page.tsx
         ├─ event
         │     ├─ eventEdition
         │     │  └─ page.tsx
         │     └─ eventRegistration
         │        └─ page.tsx
         ├─ App.css
         ├─ App.tsx
         ├─ index.css
         ├─ index.tsx
         ├─ main.tsx
         └─ Tailwind.Config.js
```



# フロント(React)画面の作成

非同期でデータを取得するため`useEffect`ライブラリを使用します。

```frontend/src/dashboard/page.tsx
useEffect(()=>{
        const fetchEvents = async()=>{
            try{
                const response = await axios.get(
                    "http://localhost:8000/api/eventAllRead",
                    {
                        headers:{
                            'Cache-Control': 'no-cache', // キャッシュを無効化
                            Pragma: 'no-cache',
                        }
                    }
                
                );
                if(response.request.status === 200){
                    setAllEvent(response.data); // 正しいデータを取得
                }
            }catch(error){
                console.error("イベントの取得に失敗しました",error);
                alert("イベントの取得に失敗しました");
            }
        };
        fetchEvents();
    },[]);// 空の依存配列で初回マウント時のみ実行
```

全体のコードはこちら↓
## 一覧画面

```frontend/src/dashboard/page.tsx
import { useNavigate } from "react-router-dom"
import Navbar from "../components/navbar/page";
import axios from "axios";
import { useEffect, useState } from "react";
import Card from "../components/card/page";

export default function dashboard(){
    //ページナビゲータを用意する
    const navigate = useNavigate();
    const backToLogin = ()=>{
        navigate('/');
    }

    const[allEvent,setAllEvent] = useState<any[]>([]);        
    
    useEffect(()=>{
        const fetchEvents = async()=>{
            try{
                const response = await axios.get(
                    "http://localhost:8000/api/eventAllRead",
                    {
                        headers:{
                            'Cache-Control': 'no-cache', // キャッシュを無効化
                            Pragma: 'no-cache',
                        }
                    }
                
                );
                if(response.request.status === 200){
                    setAllEvent(response.data); // 正しいデータを取得
                }
            }catch(error){
                console.error("イベントの取得に失敗しました",error);
                alert("イベントの取得に失敗しました");
            }
        };
        fetchEvents();
    },[]);
    

    return (
        <div>
            <Navbar></Navbar>
            ダッシュボード
            {/*カードコンポーネント*/}
            <div className="cardArea grid grid-cols-3 gap-2">                                               
                {allEvent.length > 0 && allEvent.map((event)=>( 
                    <Card event={event} key={event.eventid}></Card>
                ))}                                                                                                            
            </div>
            <div>
                <button type="button" onClick={backToLogin} className="bg-blue-500 text-white font-bold px-4 py-4">
                    ログイン画面に戻る
                </button>
            </div>
        </div>
    )
} 

```
## Cardコンポーネント
編集画面に遷移するときに①イベント名、②イベント詳細、③画像ファイルを`URLリクエストパラメータ`に入れて渡します。

```frontend/src/componenst/card/page.tsx
//処理...

const EditEvent =()=>{
        navigate(`/event/eventEdition?id=${id}&name=${name}&description=${description}`);//navigate(`/event/eventEdition&id=${event.id}`);
    }

//処理...

```

全体のコードはこちら↓

```frontend/src/componenst/card/page.tsx
import { useNavigate } from "react-router-dom"

type Event = {
    id?:number;
    name:string;
    description:string;
    thumbnail_path:string
}

export default function Card({event}){
    //ルーティング設定
    const navigate = useNavigate();

    //更新ボタンプ家事の処理
    const EditEvent =()=>{
        navigate(`/event/eventEdition?id=${id}&name=${name}&description=${description}`);//navigate(`/event/eventEdition&id=${event.id}`);
    }

    const {id,name,description,thumbnail_path} = event;
    return (
        <div>                                   
            <div className="card bg-base-100 w-96 shadow-xl">
                <div style={{visibility:"collapse"}}>{id}</div>                               
                <div className="card bg-base-100 w-96 shadow-xl">              
                    <figure>
                        <img 
                            src={`http://localhost:8000${thumbnail_path}`} 
                            alt={name || "No Image"}
                            onError={(e)=>{
                                (e.target as HTMLImageElement).src = "/default-thumbnail.jpg";
                            }}
                        />
                    </figure>
                    <div className="card-body">
                        <h2 className="card-title font-bold text-xl mb-2">{name}</h2>
                        <p className="text-base">{description}</p>
                        <div className="card-action flex items-center justify-center mt-8">
                            <button className="bg-blue-500 text-white font-bold rounded-md px-4 py-4 mr-2" onClick={EditEvent}>
                                編集する
                            </button>
                            <button className="btn btn-accent text-white bg-rose-500 font-bold rounded-md px-4 py-4">
                                削除する
                            </button>
                        </div>                   
                    </div>
                </div>                
            </div>
        </div> 
    )
}
```

## 編集画面

URLリクエストパラメータから`useSearchParams`ライブラリを使って①①イベント名、②イベント詳細、③画像名を取得しています。

```frontend/src/event/eventEdition/page.tsx
import { useNavigate,useSearchParams } from "react-router-dom"

const [searchParams] = useSearchParams();
const targetId = searchParams.get('id');
const targettName = searchParams.get('name');
const targetDescription = searchParams.get('description');
```

また、`formData`変数に①イベント名、②イベント内容、③画像パスが含まれているかを下記人します
```frontend/src/event/eventEdition/page.tsx
//フォームデータを作成する
const formData = new FormData();
formData.append('eventId',targetId);
formData.append('eventName',eventName);
formData.append('eventDescription',eventDescription);
formData.append('eventThumbnail',eventThumbnail);

console.log([...formData.entries()]); // 確認用
```

全体のソースコードはこちら↓
```frontend/src/event/eventEdition/page.tsx
import { useNavigate,useSearchParams } from "react-router-dom"
import axios from "axios";
import { useState,useEffect } from "react";
//import { useSearchParams  } from "react-router-dom";
/*
const useQuery = ()=>{
    return new URLSearchParams(useLocation().search);
}
*/

export default function EditEvent(){
    //ルーティング設定
    const navigate = useNavigate();
    //クエリパラメータを取得する
    //const query = useQuery();
    //let requestId:string = query.get('id');   
    //
    const [searchParams] = useSearchParams();
    const targetId = searchParams.get('id');//Number(query.get('id'))
    const targettName = searchParams.get('name');
    const targetDescription = searchParams.get('description');

    const [eventName,setEventName] = useState("");
    const [eventDescription,setEventDescription] = useState("");
    const [eventThumbnail,setEventThumbnail] = useState<File | null>(null);

    //一覧に戻るボタン押下時の処理
    const backTodashboard = ()=>{
        navigate('/dashboard');
    }

    useEffect(() => {

        if (targetDescription) {
            setEventName(targetDescription); // 初期値を設定
        }

        if (targetDescription) {
            setEventDescription(targetDescription); // 初期値を設定
        }


    }, [targettName, targetDescription]);

    //更新ボタン単押下時の処理
    const handleSubmit = async(e:React.FormEvent)=>{
        e.preventDefault();

        //イベントファイルが空の場合
        if(!eventThumbnail){
            alert("画像を選択してください");
            return
        }

        //フォームデータを作成する
        const formData = new FormData();
        formData.append('eventId',targetId);
        formData.append('eventName',eventName);
        formData.append('eventDescription',eventDescription);
        formData.append('eventThumbnail',eventThumbnail);

        console.log([...formData.entries()]); // 確認用

        try{
            const response = await axios.post("http://localhost:8000/api/eventEdition",formData,
            {
                headers:{
                    'Content-type':"multipart/form-data"
                }
            }
            );

            if(response.request.status != 200){
                alert("イベントの更新に失敗しました");
                return;
            }

            if(response.request.status === 200){
                alert("イベントの更新が完了しました");
                navigate('/dashboard');
            }

        }catch(error){
            console.log("error");
            alert("エラーが発生しました" + error.response.data);
        }
    }

    return (
        <form className="w-full shadow-md border-black-500 rounded-md" onSubmit={handleSubmit}>
            <div>
                <input type="hidden" value={targetId} name="eventId"/>
            </div>
            <div>
                <label className="font-bold">イベント名</label>
                <input 
                    type="text" 
                    name="eventName"
                    defaultValue={targettName || null} 
                    onChange={(e)=>setEventName(e.target.value)}
                    className="w-full border border border-gray-500 px-4 py-4 focus:bg-blue-100"
                    required
                    />
            </div>
            <div>
                <label>イベント詳細</label>
                <textarea 
                    name="eventDescription"
                    className="w-full border border-gray-500 rounded-md px-4 py-4 focus:bg-blue-100" 
                    onChange={(e)=>setEventDescription(e.target.value)}
                    defaultValue={targetDescription || null}
                    required
                    />
            </div>
            <div>
                <label className="font-bold">イベントサムネイル</label>
                <input 
                    type="file" 
                    name="eventThumbnail" 
                    accept="image/*"
                    className="block w-full text-sm text-gray-500 border border-gray-300 rounded-md font-bold px-4 py-4"
                    onChange={(e)=>setEventThumbnail(e.target.files?.[0] || null)}/>
            </div>
            <div className="flex items-center justify-center mt-8">
                <button type="submit" className="bg-blue-500 text-white font-bold rounded-md hover:bg-sky-700 px-4 py-4 mr-4 mb-8">
                    更新
                </button>
                <button type="button" className="bg-pink-500 text-white font-bold rounded-md hover:bg-pink-700 px-4 py-4  mr-4 mb-8" onClick={backTodashboard}>
                    一覧に戻る
                </button>
            </div>           
        </form>
    )
}
```

# サーバ(Laravel)側の処理

:::note info
<b>工夫した点</b>
古い画像の削除: Storage::delete() を使って、ストレージ内の古い画像ファイルを削除する処理を追加しました。これにより、ファイル名の重複を避けることができます。

画像ファイルのパス: $event->thumbnail_path で保存されている公開URLからファイル名を取得し、そのファイルがストレージに存在するか確認した後に削除します。
:::



```backend/app/Http/Controllers/EventController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;//20250110追加
use App\Models\Event;//2025.01.10追加

class EventController extends Controller
{
    public function registerEvent(Request $request)
    {
        \Log::info('リクエストデータ:', $request->all());
        $request->validate([
            'eventName' => 'required|string|max:255',
            'eventDescription' => 'required|string|max:1000',
            'eventThumbnail' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048',
        ]);

        //元のファイル名を取得する
        $originalFileName = $request->file('eventThumbnail')->getClientOriginalName();
        //ファイル名が重複しないようにタイムスタンプを追加する。
        $fileName = time().'_'.$originalFileName;

        //カスタムファイル名で画像を保存する
        $imagePath = $request->file('eventThumbnail')->storeAs('public/event_thumbnails',$fileName);
        //公開URLを生成する
        $imageUrl = Storage::url($imagePath);

        //データベースに保存する
        $event = new Event();
        $event->name = $request->eventName;
        $event->description = $request->eventDescription;
        $event->thumbnail_path = $imageUrl;
        $event->save();

        return response()->json([
            'eventName' => $event->name,
            'eventDescription' => $event->description,
            'eventThumbnail' => $imageUrl,   
        ],200);
    }

    public function editEvent(Request $request){
        \Log::info('リクエストデータ:', $request->all());
        /*これは削除しておく
        $request->validate([
            'eventId' => 'required|integer',
            'eventName' => 'required|string|max:255',
            'eventDescription' => 'required|string|max:1000',
            'eventThumbnail' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048', // 画像は必須ではない場合
        ]);
                
        if ($validator->fails()) {
            return response()->json([
                'errors' => $validator->errors()
            ], 422);
        }
        */


        $targetId = $request->get('eventId');//$request->get('eventId')

        //画像ファイル処理
        $imageUrl = null;
        //$imageUrl = $request->file('eventThumbnail')->getClientOriginalName();
        
        if($request->hasFile('eventThumbnail')){
            
            //古い画像の削除
            if($imageUrl){
                // Storage::delete()で古い画像ファイルを削除
                $imagePath = public_path('storage') . '/' . basename($imageUrl); // 正しいパスに修正
                if (file_exists($imagePath)) {
                    unlink($imagePath);  // ファイル削除
                }
            }
            
            //元のファイル名を取得する
            $originalFileName = $request->file('eventThumbnail')->getClientOriginalName();
            //ファイル名が重複しないようにタイムスタンプを追加する。
            $fileName = time().'_'.$originalFileName;
            //カスタムファイル名で画像を保存する
            $imagePath = $request->file('eventThumbnail')->storeAs('public/event_thumbnails',$fileName);
            //公開URLを生成する
            $imageUrl = Storage::url($imagePath);

        }      

        //データベースの更新
        $event = Event::find($targetId);
        if (!$event) {
            return response()->json([
                'error' => 'イベントが見つかりません'
            ], 404);
        }

        $event->name = $request->eventName;
        $event->description = $request->eventDescription;
        $event->thumbnail_path = $imageUrl;
        if($imageUrl){
            $event->thumbnail_path = $imageUrl;
        }

        $event->save();
               
        return response()->json([
            'eventName' => $event->name,
            'eventDescription' => $event->description,
            'eventThumbnail' => $event->thumbnail_path,   
        ],200);
    }

    public function readAllEvents()
    {
        $events = Event::orderBy('id','asc')->get(); // findAll()ではなくall()を使用
        return response()->json($events, 200)
                ->header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
                ->header('Pragma', 'no-cache')
                ->header('Expires', 'Thu, 01 Jan 1970 00:00:00 GMT');
        /*
        return response()->json([
            $event
        ],200);
        */
    }

}

```
以上です。