【Next.js】画面間を行き来すると画像パスが抜けるバグの解決方法

# どんなバグだったのか？
個人開発で進めている<b>①イベント一覧画面</b>と<b>②イベント編集画面</b>間でデータを受け渡しするときに
なぜか`Card`の`imgタグ`の`src`の画像パスが空文字になってしまう現象が起こっていました。

こんなかんじです↓
```
(①イベント一覧画面から②イベント編集画面へ遷移した時)
<img src={"http://localhost:3000/uploads/image.png"} alt="No Image" />
```

```
(②イベント編集画面から②イベント一覧画面へ戻った時)
<img alt="No Image"/>
```

<b>画面間の行き来するイメージ↓</b>

<b>①イベント一覧画面</b>
![編集一覧画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/c37ae77f-a505-7673-ad17-ae1033e8b4a0.png)

<b>②イベント編集画面</b>
![編集画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/dbfe285a-bfcc-65ac-6bf6-ad81381d5c85.png)

<b>①イベント一覧画面</b>
![編集一覧画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/c37ae77f-a505-7673-ad17-ae1033e8b4a0.png)

# 原因について
画面間を行き来した際に、画像パスだけが抜け落ちる原因は、データベースのカラム「eventthumbnailpath」列の値が空文字れるに上書きされることでした。

![SQL画像.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/883c19c6-1647-66b0-5153-be04843fcaa9.png)

# 対策
なので、<b>「戻る」ボタン</b>を押下した時に再度、データベースにイベントファイルパスを挿入することで解決できました。


```app/auth/eventEdit/page.tsx
'use client'

import { useRouter, useSearchParams } from "next/navigation"
import { useCallback, useEffect,useState } from "react";
import SelectEvent from "@/app/pages/api/selectEvent";
import editTargetEvent from "@/app/pages/api/editEvent";
import eventThumbnailPathSave from "@/app/pages/api/saveEventthumbnailpath";
//import { GETQueryParams } from "@/app/pages/api/urlQueryParameters";

export default function EventEdit(){
    //ルーティングの設定
    const router = useRouter();
    const [loading,setLoading] = useState(false);
    const [error,setError] = useState<string | null>(null);

    //クライアントコンポーネントフックであるuseSearchParamsを使用する
    const searchParams = useSearchParams();
    //const [targetUserId,settargetUserId] = useState(0);

    //URL Query Paramsからパラメータを取得する
    const userId = searchParams.get('userId');
    const eventId = searchParams.get('eventId');

    console.log("GETパラメータから受け取ったユーザーIDは、" + userId);
    console.log("GETパラメータから受け取ったイベントIDは、" + eventId);
        
    //状態でイベント情報を管理する
    const [targetEvent,settargetEvent] = useState<any[]>([]);//useState<any>(null);
    //画像のデフォルトURL
    const defaultImage = "/uploads/default.png";
    const [imageSrc,setImageSrc] = useState(defaultImage); 

    //let event:any;
    useEffect(()=>{
        const fetchEvent = async ()=>{
            try{
                console.log("内部のユーザーIDは、" +userId);
                const event = await SelectEvent(Number(userId),Number(eventId));
                console.log("サーバから取得してきた編集対象イベントは、" + event[0]);
                console.log("サーバから取得してきた編集対象イベントは、" + event[0].userid);
                settargetEvent(event[0]);

                console.log("useeffectの編集対象のイベントは、" + JSON.stringify(event[0]));                                                                                                                            
                if(event[0].eventthumbnailpath){
                    setImageSrc(event[0].eventthumbnailpath);
                }else{
                    setImageSrc(defaultImage);
                }
            }catch(error){
                console.error("イベントの取得中にエラーが発生しました：", error);
            }
        };
        fetchEvent();                
    },[userId,eventId]);//[userId,eventId]

    //ダッシュボードに戻るボタン押下時の処理
    const backToMyDashboard = async()=>{
        try{
            //画像サムネイルを保存する
            const saveFlag = await eventThumbnailPathSave(Number(userId), Number(eventId), imageSrc);
            if(saveFlag){
                router.push("/auth/login/dashboard");
            }else{
                console.error("画像の保存に失敗しました。");
            }
        }catch(error){
            console.error("イベントサムネイルの保存に失敗しました。", error);
        }      
        
    }

    //let jsonEvent = JSON.stringify(targetEvent);
    //console.log("外の編集対象のイベントは、" + jsonEvent);
    console.log("useEffect外の編集対象のユーザIDは、" + targetEvent.userid);
    console.log("useEffect外の編集対象のイベントタイトルは、" + targetEvent.eventtitle);            

    console.log("useEffect外の編集対象のイベントタイトル(targetEventtitle)は、" + targetEvent.eventtitle);

    //編集データを更新(登録)する
    const handleSubmit = useCallback(async (e:React.FormEvent<HTMLFormElement>)=>{
        console.log("更新ボタンを押下しました");
        e.preventDefault();
        const formData = new FormData(e.target as HTMLFormElement);
        console.log("編集後のイベント内容は、" + formData);
        const success = await editTargetEvent(formData);
        if(success){
            router.push("/auth/login/dashboard");
        }else{
            setError("イベントの更新に失敗しました。");
        }
        setLoading(false);
    },[router]);

    return (
        <div className="w-full w-96 content-center">
            <div>
                編集画面
            </div>
            <form onSubmit={handleSubmit} className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4" encType="multipart/form-data">
                <div className="userIdArea">
                    <input type="hidden" name="userId" value={targetEvent.userid || ''}/>                   
                </div>
                <div className="eventIdArea">
                    <input type="hidden" name="eventId" value={targetEvent.eventid || ''}/>
                </div>
                <div className="eventTitleArea">
                    <input
                        type="text"
                        name="eventTitle" 
                        defaultValue={targetEvent.eventtitle}
                        className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight"                                           
                    />
                    
                </div>
                <div className="eventDetailArea">
                    <textarea
                        name="eventDetail"
                        defaultValue={targetEvent.eventdetail}
                        className="block p-2.5 w-full text-sm text^gray-900 border border-gray-300"
                    ></textarea>
                </div>
                <div className="eventThumbnailArea">                     
                    <img src={imageSrc} alt="Event Thumbnail" className="w-full h-auto" />
                    <input type="file" name="eventThumbnailPath" className="file-input file-input-bordered file-input-primary w-full" />
                    <input type="hidden" name="eventThumbnailPath" defaultValue={targetEvent.eventthumbnailpath} />
                </div>
                <div className="eventEditArea">
                    <button type="submit" className="btn btn-primary" disabled={loading}>
                        {loading ? "更新中...":"更新する"}
                    </button>
                    <button className="btn btn-accent">クリアする</button>
                    <button className="btn btn-secondary" onClick={backToMyDashboard}>戻る</button>
                </div>
            </form>            
        </div>
    )
}

```

```app/pages/api/saveEventThumbnailPath.ts
'use server'
import { getDB } from "@/app/lib/db"

export default async function eventThumbnailPathSave(userId:Number,eventId:Number,eventThumbnailPath:string){
    //データベースと接続する
    const connection = await getDB();
    //保存完了フラグを定義する
    let saveEventThumbnailPath = false;

    try{
        await connection.execute('UPDATE eventinfo SET eventthumbnailpath = ? WHERE userid = ? AND eventid = ?',[userId,eventId,eventThumbnailPath]);
        const getData = connection.query('SELECT eventthumbnailpath FROM eventinfo WHERE userid = ? AND eventid = ?',[userId,eventId]);
        if(getData != null){
            console.log("イベントサムネイルパスを保存しました。");
        }
        saveEventThumbnailPath = true;
        return saveEventThumbnailPath;
    }catch(error){
        console.log("イベントサムネイルパスを保存できませんでした。");
        
    }finally{
        connection.end();
    }
    return saveEventThumbnailPath;
}
```

# チャットGPTからの回答
<b>（質問）関数内で非同期処理する方法を教えて</b>
backToMyDashboard関数内でuseEffectやuseCallbackを使うとエラーが発生する理由は、Reactのフック（useEffectやuseCallback）はコンポーネントの最上位で呼び出さなければならず、関数内で呼び出すことができないためです。これを解決するためには、非同期のデータベース処理をuseEffect内で実行し、その処理が終わった後にルーティングを行う形に変更します。

具体的には、以下の手順で修正します：

backToMyDashboardをuseEffect内で非同期処理として実行するように修正。
画像の保存処理（eventThumbnailPathSave）が終わった後にrouter.pushを呼び出す。
以下はその修正案です：

修正後のコード
```page.tsx
'use client'

import { useRouter, useSearchParams } from "next/navigation"
import { useEffect, useState } from "react";
import SelectEvent from "@/app/pages/api/selectEvent";
import editTargetEvent from "@/app/pages/api/editEvent";
import eventThumbnailPathSave from "@/app/pages/api/saveEventthumbnailpath";

export default function EventEdit() {
    const router = useRouter();
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState<string | null>(null);

    const searchParams = useSearchParams();
    const userId = searchParams.get('userId');
    const eventId = searchParams.get('eventId');

    const [targetEvent, setTargetEvent] = useState<any>({});
    const defaultImage = "/uploads/default.png";
    const [imageSrc, setImageSrc] = useState(defaultImage); 

    useEffect(() => {
        const fetchEvent = async () => {
            try {
                const event = await SelectEvent(Number(userId), Number(eventId));
                setTargetEvent(event[0]);
                if (event[0].eventthumbnailpath) {
                    setImageSrc(event[0].eventthumbnailpath);
                } else {
                    setImageSrc(defaultImage);
                }
            } catch (error) {
                console.error("イベントの取得中にエラーが発生しました：", error);
            }
        };
        fetchEvent();
    }, [userId, eventId]);

    // ダッシュボードに戻るボタン押下時の処理
    const backToMyDashboard = async () => {
        try {
            // 画像サムネイルを保存
            const saveFlag = await eventThumbnailPathSave(Number(userId), Number(eventId), imageSrc);
            if (saveFlag) {
                router.push("/auth/login/dashboard");
            } else {
                console.error("画像の保存に失敗しました");
            }
        } catch (error) {
            console.error("イベントサムネイルの保存に失敗しました。", error);
        }
    };

    const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        const formData = new FormData(e.target as HTMLFormElement);
        const success = await editTargetEvent(formData);
        if (success) {
            router.push("/auth/login/dashboard");
        } else {
            setError("イベントの更新に失敗しました。");
        }
        setLoading(false);
    };

    return (
        <div className="w-full w-96 content-center">
            <div>編集画面</div>
            <form onSubmit={handleSubmit} className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4" encType="multipart/form-data">
                <div className="userIdArea">
                    <input type="hidden" name="userId" value={targetEvent.userid || ''} />
                </div>
                <div className="eventIdArea">
                    <input type="hidden" name="eventId" value={targetEvent.eventid || ''} />
                </div>
                <div className="eventTitleArea">
                    <input
                        type="text"
                        name="eventTitle"
                        defaultValue={targetEvent.eventtitle}
                        className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight"
                    />
                </div>
                <div className="eventDetailArea">
                    <textarea
                        name="eventDetail"
                        defaultValue={targetEvent.eventdetail}
                        className="block p-2.5 w-full text-sm text^gray-900 border border-gray-300"
                    ></textarea>
                </div>
                <div className="eventThumbnailArea">
                    <img src={imageSrc} alt="Event Thumbnail" className="w-full h-auto" />
                    <input type="file" name="eventThumbnailPath" className="file-input file-input-bordered file-input-primary w-full" />
                    <input type="hidden" name="eventThumbnailPath" defaultValue={targetEvent.eventthumbnailpath} />
                </div>
                <div className="eventEditArea">
                    <button type="submit" className="btn btn-primary" disabled={loading}>
                        {loading ? "更新中..." : "更新する"}
                    </button>
                    <button className="btn btn-accent">クリアする</button>
                    <button className="btn btn-secondary" onClick={backToMyDashboard}>戻る</button>
                </div>
            </form>
        </div>
    );
}

```
<b>修正内容のポイント：</b>
backToMyDashboard関数内で非同期処理を直接実行するため、async/awaitを使用しています。
画像保存の処理（eventThumbnailPathSave）が成功した場合のみ、router.pushでダッシュボードに遷移します。
useEffectをbackToMyDashboard内ではなく、コンポーネント内のイベントデータ取得のために使っています。
これで、backToMyDashboardボタンをクリックした際に非同期処理が正しく動作し、データベースへの保存後にダッシュボード画面に遷移することができます。