[React + Firebase Authentication](後編)
ワンクリックログイン機能とルーティングの設定

こんにちは!新卒エンジニアの佐々木千奈です!

“Firebase Authentication”は、Googleが運営するサービスで、ユーザー認証機能を提供し、ユーザ情報をクラウドで保存することができます。

この記事では、前編と後編に分けてReact + Firebase Authentication を利用してログイン機能を実装する方法をご共有します。

今回は後編です。recoil、react-router-dom、firebaseを組み合わせ、ワンクリックでgoogleログインする方法を紹介していきます。

前回の記事をまだ見ていない方は是非以下の前編もチェックしてください!

今回使用した環境

  • node.js v16.15.0
  • yarn v1.22.19
  • React v18.2.0
  • TypeScript
  • firebase v9.10.0
  • react-router-dom v6.4.1
  • recoil v0.7.5

recoilの導入

今回は、状態管理ライブラリとしてrecoilを使用します。

Recoilは、Reactの提供元であるFacebook改めMetaが開発中の新しい状態管理ライブラリです。

Recoilを使うと、ReactのuseStateのような書き方で状態を保持するhookをコンポーネントをまたいで利用することができます。

atomの設定

今回は src/store/AuthState.tsに ログイン情報を保持するatomを作成します。

以下のようにatomを定義してください。

import { atom } from "recoil";

export type User = {
  id: string;
  name: string | null;
  email: string | null;
};

export type AuthState = User | null;

export const authState = atom<AuthState>({
  key: "authState",
  default: null,
});

RecoilRootの設定

Recoilを使うコンポーネントは全て<RecoilRoot>で囲む必要があるため、index.tsxに以下のように記載します。

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

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <React.StrictMode>
    <RecoilRoot>
        <App />
    </RecoilRoot>
  </React.StrictMode>
);

reportWebVitals();

ログイン機能の作成

ログインページ”Login.tsx”の作成

src/pages/Login.tsxにログインページを作成してください。このファイルにワンクリックでgoogleポップアップを用いたログインを行う関数を記載します。ソースコードは以下。

import React from "react";
import { useSetRecoilState, SetterOrUpdater } from "recoil";
import { useNavigate } from "react-router-dom";
import { fireauth } from "../firebase";
import { GoogleAuthProvider, signInWithPopup } from "firebase/auth";
import { typeAuthState, authState } from "../store/AuthState";

export const Login = () => {
  const setAuth: SetterOrUpdater<typeAuthState> = useSetRecoilState(authState);
  const navigate = useNavigate();
  const provider = new GoogleAuthProvider();

  // ログイン用関数
  const onClickLogin = async () => {
    try {
      await signInWithPopup(fireauth, provider).then((res) => {
        if (res) {
          const tempUser = {
            id: res.user.uid,
            name: res.user.displayName,
            email: res.user.email,
          };
          setAuth(tempUser);
          navigate("/");
        }
      });
    } catch {
      console.log("ログインに失敗しました");
    }
  };

  return (
    <>
      <button onClick={onClickLogin}>ログイン</button>
    </>
  );
};

ログイン後のページ”Home.tsx”を作成する

src/pages/Home.tsxにログイン後のページを作成してください。

import React from "react";

export const Home = () => {
  return (
    <>
      <p>ようこそ</p>
    </>
  );
};

ルーティングの設定

ログインユーザーの情報をもとにルーティングを行う仕組みをApp.jsに記載する。

App.tsxを以下のように編集してください。

import React, { useState, useEffect } from "react";
import { Routes, Route, BrowserRouter, Navigate } from "react-router-dom";
import { useRecoilValue, useSetRecoilState } from "recoil";
import { fireauth } from "./firebase";
import { onAuthStateChanged } from "firebase/auth";

import { authState } from "./store/AuthState";
import { Home } from "./pages/Home";
import { Login } from "./pages/Login";

type Props = {
  element: React.ReactNode;
};

// ログイン前の画面(Login)を表示するためのRouter
const LoginRoute = (props: Props) => {
  const { element } = props;
  const auth = useRecoilValue(authState);

  if (auth) {
    return (
      <>
        <Navigate to="/" replace={false} />
      </>
    );
  } else {
    return <>{element}</>;
  }
};

// ログイン後の画面(Home)を表示するためのRouter
const PrivateRoute = (props: Props) => {
  const { element } = props;
  const auth = useRecoilValue(authState);

  if (auth) {
    return <>{element}</>;
  } else {
    return <Navigate to="/login" replace={false} />;
  }
};

function App() {
  const setAuth = useSetRecoilState(authState);
  const [isLoading, setIsLoading] = useState<boolean>(true);

  useEffect(() => {
    onAuthStateChanged(fireauth, async (user) => {
      if (user) {
        const tempUser = {
          id: user.uid,
          name: user?.displayName,
          email: user?.email,
        };
        setAuth(tempUser);
        console.log("ログイン成功");
      }
      setIsLoading(false);
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <BrowserRouter>
        {isLoading ? (
          <p>Loading...</p>
        ) : (
          <Routes>
            <Route path="/login" element={<LoginRoute element={<Login />} />} />
            <Route path="/" element={<PrivateRoute element={<Home />} />} />
          </Routes>
        )}
      </BrowserRouter>
    </>
  );
}

export default App;

解説

  • react-route-dom v6 では<BrowserRouter>,<Routes>,<Route>を入れ子で使う
  • LoginRoute/PrivateRouteでは認証状態をチェックしてリダイレクトを行っている
  • useEffect内ではページをマウントした際にfirebaseのonAuthStateChangedで現在のユーザーを確認してからルーティングするために、isLoadingをstateとして使います。ローディング中は”Loading…”と表示する

設定が完了すると、以下のような画面が表示されます。

ログインボタンを押すとログインが完了し、rootへリダイレクトします。Homeコンポーネントに記載した内容が表示されていることを確認してください。

機能の追加

お疲れ様でした。これで完了です!!!!

、、、、、と、言いたいところですが、ログインだけできてもログアウトができないと作成したページが1度しか試せませんね、せっかくなので、ログアウト機能も追加してみましょう。他にもドメインのアクセス制限の方法についてご紹介します。

ログアウト機能

Home.tsxにlogout機能を追加

import React from "react";
import { useNavigate } from "react-router-dom";
import { fireauth } from "../firebase";
import { signOut } from "firebase/auth";

export const Home = () => {
  const navigate = useNavigate();
  const onClickLogout = async () => {
    try {
      await signOut(fireauth);
      navigate("/login");
      console.log("ログアウトに成功しました");
    } catch {
      console.log("ログアウトに失敗しました");
    }
  };

  return (
    <>
      <p>ようこそ</p>
      <button onClick={onClickLogout}>ログアウト</button>
    </>
  );
};

Homeにログアウト関数を追加しました。コンソールを確認するとログアウトされていることが確認できますが、画面を確認するとログアウト後navigateで”/login”に移動することには失敗してしまっています。

再ローディングを行うことでログイン画面に移動することはできますが、この部分を修正していきましょう。

この原因は、App.tsxに記載したLoginRouteです。ログアウトを行ったものの、authの情報がセットされたままであるため、ルートにリダイレクトしてしまっています。そこで、ログアウトが完了したらユーザー情報をnullにする処理を追加してみましょう。

import React from "react";
import { useNavigate } from "react-router-dom";
import { fireauth } from "../firebase";
import { signOut } from "firebase/auth";
import { useSetRecoilState } from "recoil";
import { authState } from "../store/AuthState";

export const Home = () => {
  const setAuth = useSetRecoilState(authState);
  const navigate = useNavigate();
  const onClickLogout = async () => {
    try {
      await signOut(fireauth);
      setAuth(null);
      navigate("/login");
      console.log("ログアウトに成功しました");
    } catch {
      console.log("ログアウトに失敗しました");
    }
  };

  return (
    <>
      <p>ようこそ</p>
      <button onClick={onClickLogout}>ログアウト</button>
    </>
  );
};

以上のように書き換えることで、ログアウトしたらすぐにログイン画面に遷移するようになりました。

また、stateの書き換えを行うと再レンダリングが実行され、authがnullの場合はAuthRouterにより”login”にリダイレクトされるため、最終的にはnavigateの処理を省いて以下のように改良することができます。

import React from "react";
import { fireauth } from "../firebase";
import { signOut } from "firebase/auth";
import { useSetRecoilState } from "recoil";
import { authState } from "../store/AuthState";

export const Home = () => {
  const setAuth = useSetRecoilState(authState);
  const onClickLogout = async () => {
    try {
      await signOut(fireauth);
      setAuth(null);
      console.log("ログアウトに成功しました");
    } catch {
      console.log("ログアウトに失敗しました");
    }
  };

  return (
    <>
      <p>ようこそ</p>
      <button onClick={onClickLogout}>ログアウト</button>
    </>
  );
};

ドメイン制限

今回、私たちが開発したシステムでは会社内の人だけが登録できるシステムにしたいという要望があったため、ドメイン制限の方法についてもご紹介しておきます。

ログイン用の関数内で読み込んだproviderにカスタムパラメーターを追加するだけで、簡単に実装できます。

“Login.tsx”内のproviderの読み込みの直後に以下のコードを追記します。

const provider = new GoogleAuthProvider();
// ここから追記
provider.setCustomParameters({
    hd: "〇〇〇.com",
  });

これでドメインの制限がかけられます。〇〇〇.comの部分に会社のドメインを入力することで、会社のメールアドレスを持つ人にアクセスを限定したWebアプリを作成することができます。

最後に

お疲れ様でした!!!

これでこのシリーズは全て完了です!!

ここまで読んで頂いた方、本当にありがとうございます。

Firebase Authenticationを利用したReactプロジェクトでのログイン機能の実装、いかがでしたでしょうか。

何かご意見、ご指摘等ありましたらコメントしていただけますと幸いです!

それではまた!

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

2人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です