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ファイルにぶち込む💪
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;
動作確認
Twitterのアカウントでログインして画像と名前を表示するだけのアプリケーションができました。
このUIはそのままに、読みやすいソースコードに分割できないか考えます。
クラス設計のベストエフォートがわからないので本当に自己流です...。ご容赦!
Firebaseの初期化処理
firebase.initializeApp()
は最初に必ず通過してほしく、かつプロジェクトが変わってもAPIKeyを書き換えるだけで済むようにファイルひとつにしておきます。
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でログインする処理を記述するクラスを作成します。
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を作成します。
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を参照して管理している状態を画面に表示するコンポーネントを作成します。
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.tsx
をProvider
で囲うルートのコンポーネントです。
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
クラスなんかを作ります。
まとめ
ソースの再利用がしやすいコードを意識してみましたがいかがでしょうか。
優しくマサカリぶん投げてくれる方がいらっしゃればご指摘お願いいたします。