React + React Router Dom v6 + Recoilでリロード時もログイン状態を維持する

Reactのログイン認証の状態維持についてハマったので備忘録として残しておきます。
バックエンドはLaravelを使用していますが今回はあまり関係がないので取り上げません。

使用技術

  • React17
  • React Router Dom v6
  • Recoil
  • Laravel9
  • MUI v5

Atom

import { atom, useRecoilState } from "recoil";

type userState = { id: string; name: string } | null;

const userState = atom<userState>({
    key: "user",
    default: null,
});

export const useUserState = () => {
    const [user, setUser] = useRecoilState<userState>(userState);

    return { user, setUser };
};

RecoilRoot

import React from "react";
import ReactDOM from "react-dom";
import { RecoilRoot } from "recoil";
import App from "./App";

ReactDOM.render(
    <RecoilRoot>
        <App />
    </RecoilRoot>,
    document.getElementById("app")
);

axios

import axios from "axios";

export const axiosApi = axios.create({
    baseURL: "http://localhost",
    withCredentials: true,
});

hooks

import { useUserState } from "@/atoms/user";
import { axiosApi } from "@/lib/axios";

const useUserAuth = () => {
    const { user, setUser } = useUserState();
    //ログイン済みか確認
    const userStatus = () => {
        return user ? true : false;
    };
    //Laravel側にログイン状態を確認
    const fetchUser = async (): Promise<boolean> => {
        if (user) {
            return true;
        }
        try {
            const res = await axiosApi.get("/api/user");
            if (!res.data) {
                setUser(null);
                return false;
            }
            setUser(res.data);
            return true;
        } catch {
            return false;
        }
    };

    return { userStatus, fetchUser };
};
export default useUserAuth;

ルーティング

ダメだった書き方

import React, { useEffect } from "react";
import { Routes, Route, Navigate, Outlet } from "react-router-dom";
import useUserAuth from "@/hooks/useAuth";
import Login from "@/components/pages/login";
import TopPage from "@/components/pages/index";
import TaskPage from "@/components/pages/tasks";

const App: React.FC = () => {
    const { userStatus, fetchUser } = useAuth();

    useEffect(() => {
        const init = async () => {
            // ログイン中か判定
            await fetchUser();
            return await fetchUser();
        };
        init();
    }, []);
    //ログインしていなかったらログインページにリダイレクトする
    const RouteAuthGuard = () => {
        return userStatus() ? <Outlet /> : <Navigate to="/login" replace />;
    };

    return (
        <Routes>
            <Route element={<RouteAuthGuard />}>
                <Route path="/" element={<TopPage />} />
                <Route path="/task" element={<TaskPage />} />
            </Route>
            <Route
                path="/login"
                element={userStatus() ? <Navigate to="/" /> : <Login />}
            />
        </Routes>
    );
};

export default App;

問題点

最初ルーティングのところを上記のように記述していました。
最初にログイン状態を確認し、ログインしていればログイン済みのページを表示させ、ログインしていなければログインページにリダイレクトさせます。
この実装でも、/loginから順にページ遷移させる分には問題なく動作します。
しかし、上記の記述では/taskにいる状態でリロードするとログインページに飛ばされ、
その後トップページに飛ばされてしまいます。

これはLaravelから帰ってくる結果はログイン済みと判定されるのですが、/taskに直たたきでアクセスするとRecoilに認証情報をsetUserでセットする前にuserStatusでログイン状態の確認が実行されてしまうため、未ログイン状態となりログインページに飛ばされてしまいます。

上記の記述の場合、さらにログインページに遷移した後にもログイン状態の確認が実行されます。
その時にはすでにsetUserが実行されているのでログイン状態となり、ログイン後の画面であるトップページに飛ばされるようになってしまいました。

上記の実行のイメージ

ログイン済みの状態で/taskページにアクセスし、リロードしたとします。

/taskページリロード → useStatusでログイン状態の確認 → setUserがされていないため未ログイン状態 → ログインページにリダイレクト → setUser実行 → setUserの結果ログイン状態のため、ログインページからログイン後のページにリダイレクト

解決策

ログイン状態の確認が終わったかどうかを判定するステートを用意します。
これによりログイン状態の確認が完了するまでログインチェックが実行されなくなります。
/taskページでリロードしても問題なくログイン状態が維持できるようになりました。

import React, { useEffect, useState } from "react";
import { Routes, Route, Navigate, Outlet } from "react-router-dom";
import useUserAuth from "@/hooks/useAuth";
import Login from "@/components/pages/login";
import TopPage from "@/components/pages/index";
import TaskPage from "@/components/pages/tasks";

const App: React.FC = () => {
    const { userStatus, fetchUser } = useAuth();
    // ログイン状態の確認が終わったか
    const [authChecked, setAuthChecked] = useState(false);

    useEffect(() => {
        const init = async () => {
            await fetchUser();
            return setAuthChecked(true);
        };
        init();
    }, []);
    const RouteAuthGuard = () => {
        //ログイン状態の確認が完了していればログインチェックさせる
        if (authChecked) {
            return userStatus() ? <Outlet /> : <Navigate to="/login" replace />;
        } else {
            return <></>;
        }
    };

    return (
        <Routes>
            <Route element={<RouteAuthGuard />}>
                <Route path="/" element={<TopPage />} />
                <Route path="/task" element={<TaskPage />} />
            </Route>
            <Route
                path="/login"
                element={userStatus() ? <Navigate to="/" /> : <Login />}
            />
        </Routes>
    );
};

export default App;

参考

https://qiita.com/takumi-n/items/b6d73302a54d066efb77

コメントを残す

入力エリアすべてが必須項目です。メールアドレスが公開されることはありません。

内容をご確認の上、送信してください。