スポンサーリンク
概要
Reactでログイン機能を実装した際に、画面をリロードすると毎回ログイン画面が表示されてしまう症状が発生したので対策とメモです。
スポンサーリンク
症状
React (React Hook)を使用したログイン機能をRouterを使用して実装し、認証済みの場合のみアクセス可能なページを作成します。
下記のようなカスタムしたRouter(UserAuthRoute)を作り、これを使用したページコンポーネントは認証しないとアクセスできないようにします。
実装は後述しますが、このような機能を実装した状態で、ログイン後に画面をリロードすると必ずログイン画面になってしまい、その後自動でログインされてトップページに戻るという症状が発生しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
export const App = () => ( ... <BrowserRouter> ... <Switch> <Route exact path="/" component={Home}></Route> <UserLoginRoute path="/user_login" component={UserLogin}></UserLoginRoute> <UserAuthRoute path="/greeting" component={Greeting} ></UserAuthRoute ... </Switch> <BrowserRouter> ... ); |
UserAuthRouteが、ユーザー認証が無いとログインページにリダイレクトするRouterになっています。
認証状態は、UserAuthContextといったカスタムのコンテキストを作ってそちらで管理しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import React from "react"; import { Route, Redirect } from "react-router-dom"; import { useUserAuth } from "../../contexts/UserAuthContext"; export default function UserAuthRoute({ component: Component, ...rest }: any) { const { state } = useUserAuth(); return ( <Route {...rest} render={(props) => { return state.isAuthrized ? ( <Component {...props} /> ) : ( <Redirect to={"user_login" + props.location?.pathname} /> ); }} ></Route> ); } |
ログインページに使用している、UserLoginRouteは認証済みであれば、ログインページにアクセスさせないようにログイン済みであればトップページにリダイレクトするようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import React from 'react' import { Route, Redirect } from 'react-router-dom' import { useUserAuth } from "../../contexts/UserAuthContext" export default function LoginRoute({ component: Component, ...rest }: any) { const { state } = useUserAuth() return ( <Route {...rest} render={(props) => { return state.isAuthrized ? <Redirect to="/" /> : <Component {...props} /> }} ></Route> ) } |
スポンサーリンク
原因
下記のようなログイン状態を管理するContextProviderにおいて、初期ロード状態を定期するloadingを使用して描画を止めていない事が原因でした。
{!loading && children}
といった形で、loadingがtrueになるまで子の描画を行わないようにする事で解決しました。
これを行わないと起動直後の認証が終わる前に、UserAuthRouteなどで認証状態のチェックが行われてしまい、結果として未認証としてログインページにリダイレクトされてしまうという症状が出ていました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
import * as React from "react"; import { createContext, useContext, useState, useEffect } from "react"; import { UserAuthResult, UserAuthService } from "../lib/UserAuth"; // data define type StateType = { isAuthrized: boolean; }; type ContextType = { state: StateType; login: (passCode: string) => UserAuthResult; logout: () => void; }; const initialState = { isAuthrized: false }; const UserAuthContext = createContext({} as ContextType); export function useUserAuth(): ContextType { return useContext(UserAuthContext); } export const UserAuthProvider = ({ children }: any) => { const [state, setState] = useState(initialState); const [loading, setLoading] = useState(false); function login(id: string, passCode: string): UserAuthResult { // ここにログイン認証を実装 ... setState({ ...state, isAuthrized: result.isSucessful }); return result; } function logout(): void { // ここにログアウトを実装 ... setState({ ...state, isAuthrized: false }); } useEffect(() => { setLoading(true); // ここに初期認証を実施 ... setLoading(false); }, []); const values = { state, login, logout, }; return ( <UserAuthContext.Provider value={values}> {!loading && children} </UserAuthContext.Provider> ); }; |
おまけ
上記のような実装を行うと、初期認証時に画面が真っ白になってしまうので、下記のようにローディング画面を入れた方が良いです。
(lodingの時には専用のhtml要素を返す形です。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
import * as React from "react"; import { createContext, useContext, useState, useEffect } from "react"; import { UserAuthResult, UserAuthService } from "../lib/UserAuth"; // data define type StateType = { isAuthrized: boolean; }; type ContextType = { state: StateType; login: (passCode: string) => UserAuthResult; logout: () => void; }; const initialState = { isAuthrized: false }; const UserAuthContext = createContext({} as ContextType); export function useUserAuth(): ContextType { return useContext(UserAuthContext); } export const UserAuthProvider = ({ children }: any) => { const [state, setState] = useState(initialState); const [loading, setLoading] = useState(false); ... if (loading) { return (// ここにスピナー等のを作成<>Loading...</>) return ( <UserAuthContext.Provider value={values}> {!loading && children} </UserAuthContext.Provider> ); |