Nextjs,TypeScriptでFirebaseを使ってGoogleログイン機能を実装!
firebase を使って Google ログイン機能を実装します!
使う言語は Nextjs,TypeScript です
難易度はぐぐんと上がりますが
なるべくわかりやすく解説していきます
デモサイトです
デモサイト
参考記事
準備
まずはプロジェクトの作成です。
// npmの場合
$ npx create-next-app --ts
// yarnの場合
$ yarn create next-app --typescript
続いて Firebase からプロジェクトを作成を行います。
左上のプロジェクトを作成ボタンを押してプロジェクト名を入力してください
今回はログイン機能だけなので僕のプロジェクト名は login としました。
続いてアナリティクス設定ですが今回使わないので無効にしておきます
そして右下のプロジェクト作成ボタンを押します。
新しいプロジェクトの準備ができましたら続行ボタンを押して firebaseTop ページにいきます。
TOP ページにいきましたら、左側にある構築のタグまたは TOP のすぐ下にある
Authentication を押します。
始めるボタンを押して、ログインプロバイダから Google を選択します。
有効にするボタンを押してサポートメールを設定し、保存ボタンを押します。
続いて左のタブの歯車ボタンからプロジェクトの設定を選びます。
下の方にあるマイアプリから Web を選択します。
アプリのニックネームを入力し(今回は login としました。),
Firebase SDK の追加の部分で npm を使用するを選びます。
firebaseConfig 見えなくしていますが
それぞれ表示されるのでこれをコピーします。
Firebase の初期化
まず Firebase をインストールします。
// npmの場合
$ npm install firebase
// yarnの場合
$ yarn add firebase
次に初期化を行うため、lib ディレクトリーを作成し,
firebase.ts
というファイルを作成します。
import { initializeApp, getApps } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getAuth } from "firebase/auth";
const firebaseConfig = {
apiKey: 'xxx',
authDomain: 'xxx',
projectId: 'xxx',
storageBucket: 'xxx',
messagingSenderId: 'xxx',
appId: 'xxx',
};
if (!getApps()?.length) {
initializeApp(firebaseConfig);
}
export const auth = getAuth();
export const db = getFirestore();
1 つ 1 つ解説していきます。
import { initializeApp, getApps } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getAuth } from "firebase/auth";
認証に必要な要素をインポートしています。initializeApp, getApps は
firebase/app
から getFirestore はfirebase/firestore
getAuth は firebase/auth
からインポートします。
const firebaseConfig = {
apiKey: 'xxx',
authDomain: 'xxx',
projectId: 'xxx',
storageBucket: 'xxx',
messagingSenderId: 'xxx',
appId: 'xxx',
};
Firebase と連携するための情報を入力します。
先程の画像の FirebaseConfig から内容をコピーします。
if (!getApps()?.length) {
initializeApp(firebaseConfig);
}
このコードは Firebase がすでに初期化されているかチェックして、
初期化されていない場合初期化をする関数です。
!getApps()?.length
は現在アプリが 0 である場合に true を返し、
初期化をするようにしています。
export const auth = getAuth();
export const db = getFirestore();
これらのコードは Firebase の認証と Firebase のデータベースを初期化して
エクスポートしています。
ログイン・ログアウト機能実装
lib ディレクトリ配下にauth.ts
ファイルを作成し、認証関連を入力します。
import {
GoogleAuthProvider,
signInWithPopup,
UserCredential,
signOut,
} from 'firebase/auth';
import { auth } from "./firebase";
export const login = (): Promise<UserCredential> => {
const provider = new GoogleAuthProvider();
return signInWithPopup(auth, provider);
};
export const logout = (): Promise<void> => {
return signOut(auth);
};
今回も 1 つ 1 つ解説していきます。
import {
GoogleAuthProvider,
signInWithPopup,
UserCredential,
signOut,
} from 'firebase/auth';
import { auth } from "./firebase";
Google アカウントを使用した認証に必要なGoogleAuthProvider
,
認証ポップアップを表示するsignInWithPopup
,
認証に成功したユーザー情報を取得するUserCredential
,
ユーザーをログアウトするための機能signOut
を
firebase/auth
より
先程のfirebase.ts
で定義したauth
をインポートします
export const login = (): Promise<UserCredential> => {
const provider = new GoogleAuthProvider();
return signInWithPopup(auth, provider);
};
Firebase にログインするための関数 login を定義し Promise を使用して
非同期処理を行いその結果としてユーザー情報を取得する UserCredential を返します。
provider という定数に new を使用して GoogleAuthProvider オブジェクトを作成します。
signInWithPopup でポップアップウィンドウで Google ログインの処理を行います。
最後にログインが成功すると非同期処理を行っていた Promise を返します。
export const logout = (): Promise<void> => {
return signOut(auth);
};
ログアウトするための関数です。
signOut(auth)
で現在認証されているユーザーをログアウトし
最後非同期処理でPromise<void>
を返しログアウトを完了させます。
認証コンテキストの作成
続いてログイン状態かログアウト状態かなどの情報を保持する認証コンテキストを作成します。
まずはユーザーの型を作成します。
types
フォルダを作成し、user.ts
ファイルを作成します。
export type User = {
id: string;
name: string;
};
User という型を定義し id と name を
string
で定義します。
続いてコンテキストを作成します。
context
フォルダを作成し、その中にauth.tsx
ファイルを作成します
import { User } from "@/types/user";
import { createContext } from "react";
type UserContextType = User | null | undefined;
const AuthContext = createContext<UserContextType>(undefined);
1 つ 1 つ解説します。
import { User } from "@/types/user";
import { createContext } from "react";
先程user.ts
で設定したUser
新しいコンテキストを作成するcreate
を react からインポートします。
type UserContextType = User | null | undefined;
UserContextType という新しい型をユニオン型で定義しています。
ログイン中の場合はUser
をログインをしていない場合はnull
を
ローディング中はundefined
を返しそれぞれにあった UI を返します。
const AuthContext = createContext<UserContextType>(undefined);
undefined
を初期値にもった AuthContext という新しいコンテキストを
作成しています。型は先程定義したUserContextType
を指定しています。
中身の Provider を書いて行きます。
import { auth, db } from "@/lib/firebase";
import { User } from "@/types/user";
import { doc, getDoc, setDoc } from "@firebase/firestore";
import { onAuthStateChanged } from "firebase/auth";
import {
ReactNode,
createContext,
useContext,
useEffect,
useState,
} from "react";
type UserContextType = User | null | undefined;
const AuthContext = createContext<UserContextType>(undefined);
// 以下を追加
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<UserContextType>();
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (firebaseUser) => {
if (firebaseUser) {
const ref = doc(db, `users/${firebaseUser.uid}`);
const snap = await getDoc(ref);
if (snap.exists()) {
const appUser = (await getDoc(ref)).data() as User;
setUser(appUser);
} else {
const appUser: User = {
id: firebaseUser.uid,
name: firebaseUser.displayName!,
};
setDoc(ref, appUser).then(() => {
setUser(appUser);
});
}
} else {
setUser(null);
}
return unsubscribe;
});
}, []);
return <AuthContext.Provider value={user}>{children}</AuthContext.Provider>;
};
export const useAuth = () => useContext(AuthContext);
export const AuthProvider = ({ children }: { children: ReactNode }) => {
//省略
return <AuthContext.Provider value={user}>{children}</AuthContext.Provider>;
}
まず AuthProvider は受け取った children という props を使用して
ラップされた要素全てに認証情報を提供します。
型は React のプロパティ型の1つの ReactNode を指定しています。
最後の return 文はAuthContext.Provider
と呼ばれる
React のコンテキストを使用して、user 変数を設定し、
子要素がユーザー情報を提供できるようにしています。
const [user, setUser] = useState<UserContextType>();
useState を使用して user という変数を定義し、更新関数として setUser を指定しています。
型は先程作成したUserContextType
を指定します。
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (firebaseUser) => {
if (firebaseUser) {
const ref = doc(db, `users/${firebaseUser.uid}`);
const snap = await getDoc(ref);
if (snap.exists()) {
const appUser = (await getDoc(ref)).data() as User;
setUser(appUser);
} else {
const appUser: User = {
id: firebaseUser.uid,
name: firebaseUser.displayName!,
};
setDoc(ref, appUser).then(() => {
setUser(appUser);
});
}
} else {
setUser(null);
}
return unsubscribe;
});
}, []);
useEffect を用いて認証状態が変化したときに呼ばれる関数を作成しています。
const unsubscribe = onAuthStateChanged(auth, async (firebaseUser) => {
//省略
},[])
関数unsubscribe
にログイン、ログアウトの変化が起きたときに
コールバック関数を呼び出すonAuthStateChanged
メソッドを指定します。
第 1 引数にlib/firebase
から auth を第 2 引数に現在の認証ユーザーを
表すfirebaseUser
を引数で受け取ります。
useEffect の第 2 引数に[]
を渡すことで 1 度だけ
更新するようにしています。
const ref = doc(db, `users/${firebaseUser.uid}`);
ログインしているユーザーの情報を保持する関数 ref として
Firestore データベース内のドキュメントを示すdoc()
を使用し
users コレクション内の指定されたfirebaseUser.uid
を持つ
ドキュメントを取得します。
const snap = await getDoc(ref);
snap という定数に非同期処理でドキュメントの内容を取得する
getDoc 関数を使います。
引数として先程のユーザーの情報を保持している ref をしています。
if (snap.exists()) {
const appUser = (await getDoc(ref)).data() as User;
setUser(appUser);
}
先程の定数と Firestore のDocumentSnapshot
オブジェクトのメソッド
exists()
を用いてユーザーがドキュメントに存在しているかチェックします。
もしドキュメントに存在している場合、
appUser としてUser
型を定義し、
先程と同様に getDoc 関数を使いdata()
で取得された
ドキュメントデータを取得、最後に state の更新関数である
setUser に渡します。
else {
const appUser: User = {
id: firebaseUser.uid,
name: firebaseUser.displayName!,
};
setDoc(ref, appUser).then(() => {
setUser(appUser);
});
}
もしドキュメントに存在していない場合 Firestore に
ユーザー情報を保存する処理します。
appUser で新しいユーザーの情報をオブジェクト形式で
作成し、その後setDoc(ref,appUser)
で Firestore の
users コレクション内に新しいユーザーを保存します。
保存が完了したらthen()
メソッドが呼ばれ
setUser(appUser)
でアプリの状態を更新します。
else {
setUser(null);
}
ログアウトした場合にアプリのユーザーを初期化します。
return unsubscribe;
このコンポーネントが不要になったら
監視を終了させます。
これによりメモリーリークを防止できます。
export const useAuth = () => useContext(AuthContext);
最後にコンテキストのデータを受け取るためにuseContext
を実装します。
必要な都度useContext(AuthContext);
と書いてもいいですが、
短くするためにuseAuth
を指定します。
ReactHook なので関数内で使用する必要があるので
アロー関数を使用しています。
Provider を書き終わったので、次は全体で
ユーザー情報を供給するために_app.tsx
で全体を囲みます。
+import { AuthProvider } from "@/context/auth";
import type { AppProps } from "next/app";
export default function App({ Component, pageProps }: AppProps) {
return (
+ <AuthProvider>
<Component {...pageProps} />
+ </AuthProvider>
);
}
続いて Firebase でセキュリティルールを設定します。
Firestore からログインしたユーザーのデータを読み込むのと
ユーザーデータがない場合 Firestore にユーザーデータを作成する
ルールを設定します。
これの設定を行わないとログインした際に Firebase に対して
認証されていませんとエラーが出てしまいます。
Firebase の左のメニューの構築から
Firestore Database を選択します。
データベースの作成ボタンを押し、
本番環境モードで開始するを選択し、右下の次へボタンを選択します。
次にロケーションの設定を指定されるので
asia-northeast1(Tokyo)
を選択します。
住んでる場所が大阪に近ければ
asia-northeast2(Osaka)
を選択してください。
選択しましたら有効にするを押します
データベースを作成したら上にあるタブのルールを選択します。
ルールを編集ボタンを押し次のコードに置き換えます。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{uid} {
allow read, write: if request.auth != null && request.auth.uid == uid;
}
}
}
解説していきます。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
rules_vearsion
はセキュリティのバージョンを定義しています。
service cloud.firestore
は Cloud Firestore サービスに対する
ルールであることを指定しています。
match /databases/{database}/documents
は
Firebase プロジェクト内すべてのデータベースに対して
セキュリティルールを適用することをあらわしています。
match /users/{uid} {
allow read, write: if request.auth != null && request.auth.uid == uid;
}
users
コレクション内の読み込み(read)と書き込み(write)の
権限を設定しています。
request.auth がnull
ではなくかつ、request.auth.uid
とuid
が一致する場合に
ユーザーが自分のデータにアクセスできる様になります。
最後にindex.jsx
にログインボタンとログアウトボタンを作成します。
import { useAuth } from "@/context/auth";
import { login, logout } from "@/lib/auth";
import { useState } from "react";
export default function Home() {
const user = useAuth();
const [waiting, setWaiting] = useState<boolean>(false);
const signIn = () => {
setWaiting(true);
login()
.catch((error) => {
console.error(error?.code);
})
.finally(() => {
setWaiting(false);
});
};
return (
<div>
{user === null && !waiting && <button onClick={signIn}>ログイン</button>}
{user && <button onClick={logout}>ログアウト</button>}
</div>
);
}
const user = useAuth();
const [waiting, setWaiting] = useState<boolean>(false);
まず認証されたユーザーの情報をuser
に保存し、
useState を使ってwaiting
という state を初期値は false で作ります。
ログイン後 waiting の値が変更されるので更新関数の
setWaiting
も定義します。
const signIn = () => {
setWaiting(true);
login()
.catch((error) => {
console.error(error?.code);
})
.finally(() => {
setWaiting(false);
});
};
signIn
関数が実行されたときwaiting
を true に設定します。
lib/auth.ts
で設定したlogin
関数で
非同期処理の Promise を使用しているため
catch()
とfinally()
を使って Promise が完了したあとに
実行する処理を行っています。
catch()
でエラーが発生した際にエラーコードを
finally()
で waiting を false に設定しています。
<div>
{user === null && !waiting && <button onClick={signIn}>ログイン</button>}
{user && <button onClick={logout}>ログアウト</button>}
</div>
ユーザーがログインしている場合はログアウトボタンを、
ユーザーがログインしていない、かつwaiting
が false の場合
signIn
関数が呼び出されログインボタンを
表示させます。
これで完成です!
さいごに
お疲れさまでした!
Firebase と Nextjs で Google ログイン機能は
とても複雑で難しかったです。
伝わりにくかった部分もあったと思いますが
ご了承ください。
最後までお読みいただきありがとうございました。
Discussion
失礼します
context/auth.tsx内のuseEffect内のonAuthStateChangedが受け取るコールバック関数内で外側の自分自身をreturnしようとしていますが、これは以下の誤りだと思われます。
この記事を見て困っている方がいたので修正していただけると幸いです。