投稿日:2021年5月30日
この記事ではContextでログイン状態・認証情報を管理する方法について詳しく解説しています。以前に自分がYoutubeに投稿した『React勉強垂れ流し動画』内で同じ方法で実装しました。
この記事ではContextでログイン状態・認証情報を管理する方法について詳しく解説しています。以前に自分がYoutubeに投稿した『React勉強垂れ流し動画』内で同じ方法で実装しました。
この記事は以下の環境で確認されました。
まずは作成する機能の条件を列挙しておきます。
条件の3を満たす方法はいくつかあるのですが、今回はReactのデフォルトの機能であるContextを使用してみます。
まずはログイン状態と認証情報を取得するContextをAuthContextProvider.tsxというファイルに作成してみます。Contextの作成はReact.createContext<T>()関数を使用します。
import React, { useEffect, useState } from "react";
/**
* 簡易的な認証情報の型のサンプル
*/
type AuthInfo = {
userId: string;
};
// ログイン状態のContext
export const LoggedInContext = React.createContext<boolean>(false);
// 認証情報と認証情報セットのContext
export const AuthInfoContext = React.createContext<
[AuthInfo, React.Dispatch<React.SetStateAction<AuthInfo>>]
>([{ userId: "" }, () => {}]);
LoggedInContextについては特に解説の必要がないと思います。単純にログイン状態をbooleanで型指定して、デフォルト値をfalseでセットしています。
ユーザー情報を管理するAuthInfoContextは3. どのコンポーネントからでもログイン・ログアウトが可能の条件を満たすためにほんの少しだけ複雑な作りになっています。ここでcreateContext<>()の型引数で指定している型はuseState<>()の返り値の型です。こうすることで、タプルの2つ目の関数を介してどのコンポーネントからでも自由に認証情報をセットできますね。
本当はReturnType<typeof useState<AuthInfo>>
と書きたかったのですがtypeofがgenericに対応していないため直接指定することになりました(泣)。
今作成したContextを使うためには、値を使用したいコンポーネントより外側でContextのProviderを呼び出してあげればOKです。もちろん直接ルートコンポーネントあたりに直で書いてもOKです。
今回は、可読性・メンテナンス性を考えてさらにラッパーコンポーネントを作成することにします。
先程Contextを作成したファイルに追加でラッパー関数を作成します。
[...]
export const AuthContextProvider: React.FC<{}> = (props) => {
// stateの定義
const [loggedIn, setLoggedIn] = useState<boolean>(false);
const [authInfo, setAuthInfo] = useState<AuthInfo>({ userId: "" });
// authInfoのバリデーション
useEffect(() => {
// authInfoに正しく値がセットされているかどうかをチェック
if (authInfo?.userId) {
setLoggedIn(true);
} else {
setLoggedIn(false);
}
}, [authInfo]);
return (
<LoggedInContext.Provider value={loggedIn}>
<AuthInfoContext.Provider value={[authInfo, setAuthInfo]}>
{props.children}
</AuthInfoContext.Provider>
</LoggedInContext.Provider>
);
};
このコンポーネントは子要素のみを受け取るコンポーネントとして設計しています。子要素を受け取るコンポーネントの場合、コンポーネントの型として直接 React.FC<P>の型を指定します。今回はpropsは受け取ら無いのでPには空の型を指定しています。
次にコンポーネントの中の説明に移ります。useState<T>()でさきほどのContextで指定した値をstateとして定義しています。そのうちのauthInfoはuseEffect()によって変更が監視され、正しい値が入った場合のみloggedInにtrueがセットされます。
TSX部分ではこのコンポーネントの子要素がすべてContextの値を取得できる様に{props.children}をContextのProviderで囲んでいます。
次に2.以前にログインしたことがあれば自動で認証情報を取得を考えてみましょう。
自動ログイン機能を実装する最も簡単な方法はログイン時にブラウザのローカルストレージに情報を保持しておいて、次にアクセスした際にそこから情報を取り出す方法です。
この機能を関数として実装してみましょう。
[...]
/**
* デフォルトのAuthInfoを取得
* ローカルストレージから取得できた場合はその値をパース
* 取得できない場合は空の情報を返す
* @returns
*/
function getDefaultAuthInfo(): AuthInfo {
const defaultAuthInfo = window.localStorage.getItem("authInfo");
if (defaultAuthInfo) {
return JSON.parse(defaultAuthInfo) as AuthInfo;
} else {
return { userId: "" };
}
}
/**
* 認証情報をローカルストレージに追加
* @param authInfo
*/
function setAutoInfoToLocalStorage(authInfo: AuthInfo): void {
const authInfoStringfy = JSON.stringify(authInfo);
window.localStorage.setItem("authInfo", authInfoStringfy);
}
[...]
作成した関数を先程作成したProviderコンポーネント上で呼び出します。
/**
* コンテキストのProvider
*/
export const AuthContextProvider: React.FC<{}> = (props) => {
// stateの定義
const [loggedIn, setLoggedIn] = useState<boolean>(false);
const [authInfo, setAuthInfo] = useState<AuthInfo>(getDefaultAuthInfo());
// authInfoのバリデーション
useEffect(() => {
// authInfoに正しく値がセットされているかどうかをチェック
if (authInfo?.userId) {
setAutoInfoToLocalStorage(authInfo);
setLoggedIn(true);
} else {
setLoggedIn(false);
}
}, [authInfo]);
return (
<LoggedInContext.Provider value={loggedIn}>
<AuthInfoContext.Provider value={[authInfo, setAuthInfo]}>
{props.children}
</AuthInfoContext.Provider>
</LoggedInContext.Provider>
);
};
これでページ読み込み時にはブラウザのローカルストレージからデータの読み込みを試行して、ログイン成功時にはローカルストレージにデータを書き込むようになりました!
実際に使って見ます。
まずは認証機能、情報を使用するコンポーネントをProviderラッパーコンポーネントで包みます。今回は一番ルートのindex.tsxで呼び出して見ましょう。
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { AuthContextProvider } from "./AuthContextProvider";
ReactDOM.render(
<React.StrictMode>
<AuthContextProvider>
<App />
</AuthContextProvider>
</React.StrictMode>,
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
次に内側のコンポーネントでContextを呼び出します。
import React, { useContext } from "react";
import { LoggedInContext, AuthInfoContext } from "./AuthContextProvider";
function App() {
const isLoggedIn = useContext(LoggedInContext);
const [authInfo, setAuthInfo] = useContext(AuthInfoContext);
return (
<div>
{isLoggedIn ? `ID: ${authInfo?.userId}` : "ログインされていません"}
<button
onClick={() => {
setAuthInfo({ userId: "abcdefg123455" });
}}
>
ログイン
</button>
<button
onClick={() => {
setAuthInfo({ userId: "" });
}}
>
ログアウト
</button>
</div>
);
}
export default App;
この状態でログイン、ログアウトボタンをクリックしたりページ読み込みをしたりしてみてください。うまく認証情報が扱えていることが確認できると思います!
これはあくまで非常に簡単にした使用例であることに注意してください。実際にはログインフォームをサーバーにPOSTして、Responseとして受け取ったUserIdやセッションIDをセットします。
もっと深い子要素のコンポーネントを作成してもuseContextで作成したContextを呼び出せば認証情報・ログイン状態を自由に取得できます!確認してみましょう!!