Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
6
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

@stin_dev

【React+Firebase Auth】ログイン周りのクラス設計を考える

ReactとFirebase Authenticationのアプリでログイン周りの最適なクラス設計を考えたい。

Global Stateの管理はunstatedを使っていましたが、同じ作者がReact Hooksを用いたunstated-nextを開発されていたのでそちらに乗り換えます。

前提

Windows10で作成します。
アプリの土台はcreate-react-appで作成しています。

Twitterログインだけを実装します。Twitterアプリの登録は済ませてあります。

とりあえず動くものを

npx create-react-appを実行した後、必要なパッケージを導入します。

npm install unstated-next
npm install firebase

まずは馬鹿正直に一つのtsxファイルにぶち込む💪

src/App.tsx
import React from 'react';
import './App.css';
import { createContainer } from "unstated-next";
import firebase from "firebase/app";
import "firebase/auth";

firebase.initializeApp({
  apiKey: "this-is-your-api-key",
  authDomain: "your-app-name.firebaseapp.com",
  databaseURL: "https://your-app-name.firebaseio.com",
  projectId: "your-app-name",
  storageBucket: "your-app-name.appspot.com",
  messagingSenderId: "999999999999",
  appId: "9:999999999999:web:999xxx999xxx999xxx999x",
  measurementId: "Z-ZZZZZZZZ999"
});

function useUserState() {
  const [user, setUser] = React.useState<firebase.User | null>(null);

  firebase.auth().onAuthStateChanged(user => setUser(user));

  const signIn = async () => {
    try {
      const provider = new firebase.auth.TwitterAuthProvider();
      await firebase.auth().signInWithPopup(provider);
    } catch (error) {
      setUser(null);
    }
  }

  const signOut = async () => {
    await firebase.auth().signOut();
  }

  return { user, signIn, signOut }
}

const userContainer = createContainer(useUserState);

const LoginPage = () => {
  const userState = userContainer.useContainer();
  return (
    <div className="App">
      {userState.user
        ?
        <div className="isSignedIn">
          <div className="rowContent">
            <button
              className="signInButton"
              onClick={userState.signOut}>
              サインアウト
          </button>
          </div>
          <div className="rowContent">
            <img src={userState.user.photoURL ? userState.user.photoURL.replace("normal", "200x200") : ""} />
          </div>
          <div className="rowContent">
            <p>{userState.user.displayName}</p>
          </div>
          <div className="rowContent">
            <p>{userState.user.uid}</p>
          </div>
        </div>
        :
        <div className="rowContent">
          <button
            className="signInButton"
            onClick={userState.signIn}>
            サインイン
        </button>
        </div>
      }
    </div>
  );
}

const App: React.FC = () => {
  return (
    <userContainer.Provider>
      <LoginPage />
    </userContainer.Provider>
  );
}

export default App;

動作確認

login-app.gif

Twitterのアカウントでログインして画像と名前を表示するだけのアプリケーションができました。
このUIはそのままに、読みやすいソースコードに分割できないか考えます。

クラス設計のベストエフォートがわからないので本当に自己流です...。ご容赦!

Firebaseの初期化処理

firebase.initializeApp()は最初に必ず通過してほしく、かつプロジェクトが変わってもAPIKeyを書き換えるだけで済むようにファイルひとつにしておきます。

src/firebase.ts
import firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";

firebase.initializeApp({
  apiKey: "this-is-your-api-key",
  authDomain: "your-app-name.firebaseapp.com",
  databaseURL: "https://your-app-name.firebaseio.com",
  projectId: "your-app-name",
  storageBucket: "your-app-name.appspot.com",
  messagingSenderId: "999999999999",
  appId: "9:999999999999:web:999xxx999xxx999xxx999x",
  measurementId: "Z-ZZZZZZZZ999"
});

export default firebase;

このソースがexportするfirebaseを参照すればinitializeApp()済みであることが保証されます。
importは利用するFirebaseのサービスに合わせて追加削除します。

ちなみにinitializeApp()に渡しているAPIKeyは第三者に知られても問題ないようです。GitHubのパブリックなリポジトリにこのままプッシュできて楽ですね。

ログイン処理

Firebase Authenticationでログインする処理を記述するクラスを作成します。

src/service/AuthService.ts
import firebase from "../firebase";

export class AuthService {
    onAuthStateChanged = (observer: (user: firebase.User | null) => void) => {
        firebase.auth().onAuthStateChanged(observer);
    }

    signInWithTwitter = async () => {
        const provider = new firebase.auth.TwitterAuthProvider();
        await firebase.auth().signInWithPopup(provider);
    }

    signOut = async () => {
        await firebase.auth().signOut();
    }
}

必要に応じてsignInWithGoogleなどを追加します。エラー処理もしてあるとベストです(手抜き)。

Container

unstated-nextを用いてstate管理するContainerを作成します。

src/container/UserContainer.ts
import React from 'react';
import { createContainer } from "unstated-next";
import firebase from "../firebase";
import { AuthService } from '../service/AuthService';

const useUserState = () => {
  const authService = new AuthService();
  const [user, setUser] = React.useState<firebase.User | null>(null);

  authService.onAuthStateChanged(user => setUser(user));

  const signIn = async () => { await authService.signInWithTwitter(); }

  const signOut = async () => { await authService.signOut(); }

  return { user, signIn, signOut }
}

const userContainer = createContainer(useUserState);

export default userContainer;

内部でログイン処理を委譲するAuthServiceのインスタンスを生成しています。
本当は中でnewしたくないのですがいい方法が思いつきませんでした。どなたか知見があれば教えていただけると助かります。

ログインページ

Containerを参照して管理している状態を画面に表示するコンポーネントを作成します。

src/components/LoginPage.tsx
import React from 'react';
import './App.css';
import userContainer from "../container/UserContainer";

const LoginPage = () => {
    const userState = userContainer.useContainer();

    return (
        <div className="App">
            {userState.user
                ?
                <div className="isSignedIn">
                    <div className="rowContent">
                        <button
                            className="signInButton"
                            onClick={userState.signOut}>
                            サインアウト
                        </button>
                    </div>
                    <div className="rowContent">
                        <img alt="icon"
                            src={userState.user.photoURL ? userState.user.photoURL.replace("normal", "200x200") : ""} />
                    </div>
                    <div className="rowContent">
                        <p>{userState.user.displayName}</p>
                    </div>
                    <div className="rowContent">
                        <p>{userState.user.uid}</p>
                    </div>
                </div>
                :
                <div className="rowContent">
                    <button
                        className="signInButton"
                        onClick={userState.signIn}>
                        サインイン
                    </button>
                </div>
            }
        </div>
    );
}

export default LoginPage;

UIとロジックが分離されていて読みやすくなっているかと思います。

ルート

LoginPage.tsxProviderで囲うルートのコンポーネントです。

src/App.tsx
import React from 'react';
import LoginPage from "./components/LoginPage";
import userContainer from "./container/UserContainer";

const App: React.FC = () => {
  return (
    <userContainer.Provider>
      <LoginPage />
    </userContainer.Provider>
  );
}

export default App;

Appをレンダリングすれば上の方に載せたgifと同じものがブラウザに表示されます。

ディレクトリ構成

src配下のディレクトリ構成は下記の通りになっています。

│  App.tsx
│  firebase.ts
│
├─components
│      LoginPage.css
│      LoginPage.tsx
│
├─container
│      UserContainer.ts
│
└─service
        AuthService.ts

ログイン周りに限らず、ロジックを記述するソースはserviceに、状態管理に関するソースはcontainerに保存します。
さらにFirestoreにアクセスするアプリの場合はrepositoryフォルダを作成してUserRepositoryクラスなんかを作ります。

まとめ

ソースの再利用がしやすいコードを意識してみましたがいかがでしょうか。
優しくマサカリぶん投げてくれる方がいらっしゃればご指摘お願いいたします。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
6
Help us understand the problem. What are the problem?