Next.js + TypeScript + Firebaseで認証機能を実装する

2020-12-15

※この記事は2021/05/31にリライトされました。

概要

Firebaseは、Web アプリケーションのバックエンドサービスです。
画像のアップロード、ホスティング、認証機能など様々なサービスを簡単に導入できる素晴らしいサービスです。

フロントエンド実装だけで認証機能はできません。バックエンド処理が必要になってきます。
サービスには認証機能は必須であり、避けて通れません。

今回は、Next.jsとTypeScriptを使用してFirebase Authenticationを実装してみます。

認証の種類

Firebase Authoricationでは電話番号認証や匿名などの認証形式が可能です。
今回はEmail + PassWordの組み合わせで進めていきます。

Firebaseにアプリ登録する

Firebaseコンソール からプロジェクトを追加します。

追加して管理画面をみると、サイドバーにAuthenticationがありますので、そこをクリック。
Sign-in method というタブがありますので、選び、メール/パスワードを有効にしてください。

そして、サイドバーの歯車からプロジェクト設定へと進みます。

全般タブ下にマイアプリがありますので、新規にアプリを追加してください。
追加されるとSDK snippetが表示されます(のちに使用します)。

フロント側の実装

冒頭で述べた通り、Next(React)+TSといった組み合わせです。

SDK snippetを環境変数に格納する

.envを作成、ルートに置いてください。
先ほどマイアプリで作成したSDK snippetがありました。それを環境変数に入れていきましょう。
内容は以下の通りです。

.env
FIREBASE_KEY="xxxxxx"
FIREBASE_DOMAIN="xxxxxx"
FIREBASE_DATABASE="xxxxxx"
FIREBASE_PROJECT_ID="xxxxxx"
FIREBASE_STORAGE_BUCKET="xxxxxx"
FIREBASE_SENDER_ID="xxxxxx"
FIREBASE_APPID="xxxxxx"

各ページで使用できるようにnext.config.jsに登録します。

next.config.js
module.exports = {
  env: {
    FIREBASE_KEY: process.env.FIREBASE_KEY,
    FIREBASE_DOMAIN: process.env.FIREBASE_DOMAIN,
    FIREBASE_DATABASE: process.env.FIREBASE_DATABASE,
    FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID,
    FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET,
    FIREBASE_SENDER_ID: process.env.FIREBASE_SENDER_ID,
    FIREBASE_APPID: process.env.FIREBASE_APPID
  }
}

Firebase情報を初期化する

まず、アプリの情報を登録する作業が必要です。 utils/firebase.tsを作成し初期化してみましょう。

src/utils/firebase.ts
import 'firebase/auth'
import 'firebase/firestore'

import firebase from 'firebase/app'

const config = {
  apiKey: process.env.FIREBASE_KEY,
  authDomain: process.env.FIREBASE_DOMAIN,
  databeseURL: process.env.FIREBASE_DATABASE,
  projectId: process.env.FIREBASE_PROJECT_ID,
  storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.FIREBASE_SENDER_ID,
  appId: process.env.FIREBASE_APPID,
}
// initializeを複数回走らせない
if (firebase.apps.length === 0) {
  firebase.initializeApp(config)
}
const auth = firebase.auth()
export { auth }

認証情報を共有するためのProviderを作成

次にユーザー情報を各ページで使用できるよう、フックAPIのuseContextを使用してプロバイダーを作成しましょう。

src/auth/AuthProvider.tsx
import { User } from 'firebase'
import { FC, createContext, useEffect, useState } from 'react'
import { auth } from '../utils/firebase'

type AuthContextProps = {
  currentUser: User | null | undefined
}

const AuthContext = createContext<AuthContextProps>({ currentUser: undefined })

const AuthProvider: FC = ({ children }) => {
  const [currentUser, setCurrentUser] = useState<User | null | undefined>(
    undefined
  )

  useEffect(() => {
    auth.onAuthStateChanged((user) => {
      setCurrentUser(user)
    })
  }, [])

  return (
    <AuthContext.Provider value={{ currentUser }}>
      {children}
    </AuthContext.Provider>
  )
}

export { AuthContext, AuthProvider }

authのonAuthStateChangedメソッドでuserの情報を取得し、ローカルステートのcurrentUserに格納します。 useContextを使用し、children(各ページ)で使用できるようにします。

トップページの実装

トップページではuser情報があるか確認し、もしなければログインページへリンクします。

index.tsx
import { useEffect, FC, useState } from 'react'
import { useRouter } from 'next/router'
import Head from 'next/head'

import { auth } from '../utils/firebase'

const Home: FC = (props: any) => {
  const router = useRouter()
  const [currentUser, setCurrentUser] = useState<null | object>(null)

  useEffect(() => {
    auth.onAuthStateChanged((user) => {
      user ? setCurrentUser(user) : router.push('/login')
    })
  }, [])

  const logOut = async () => {
    try {
      await auth.signOut()
      router.push('/login')
    } catch (error) {
      alert(error.message)
    }
  }

  return (
    <div>
      <pre>{currentUser && JSON.stringify(currentUser, null, 4)}</pre>
      <button onClick={logOut}>Logout</button>
    </div>
  )
}

export default Home

ログインページの実装

ログインでは登録したユーザーとの整合性をチェックします。
また、既にログインしている場合はトップページへリンクするようにしています。

login.tsx
import React, { useEffect, useState, FC } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { auth } from '../utils/firebase'

const Login: FC = () => {
  const router = useRouter()
  const [email, setEmail] = useState<string>('')
  const [password, setPassword] = useState<string>('')

  useEffect(() => {
    auth.onAuthStateChanged((user) => {
      user && router.push('/')
    })
  }, [])

  const logIn = async (e) => {
    e.preventDefault()
    try {
      await auth.signInWithEmailAndPassword(email, password)
      router.push('/')
    } catch (err) {
      alert(err.message)
    }
  }

  return (
    <div className="wrapper">
      <form className="auth" onSubmit={logIn}>
        <div>
          <label htmlFor="email" className="auth-label">
            Email:{' '}
          </label>
          <input
            id="email"
            className="auth-input"
            type="email"
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div className="mt-2">
          <label htmlFor="password" className="auth-label">
            Password:{' '}
          </label>
          <input
            id="password"
            className="auth-input"
            type="password"
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <button className="auth-btn" type="submit">
          Login
        </button>
      </form>
      <Link href="/signup">
        <a className="auth-link">signup</a>
      </Link>
    </div>
  )
}

export default Login

サインアップページの実装

ユーザーを登録する際のページになります。

signup.tsx
import { FC, useEffect, useState } from 'react'
import Router, { useRouter } from 'next/router'
import Link from 'next/link'

import { auth } from '../utils/firebase'
import { AuthContext } from '../auth/AuthProvider'

const SignUp: FC = () => {
  const router = useRouter()
  const [email, setEmail] = useState<string>('')
  const [password, setPassword] = useState<string>('')

  useEffect(() => {
    auth.onAuthStateChanged((user) => {
      user && router.push('/')
    })
  }, [])

  const createUser = async (e) => {
    e.preventDefault()
    try {
      await auth.createUserWithEmailAndPassword(email, password)
      router.push('/login')
    } catch (err) {
      alert(err.message)
    }
  }

  return (
    <div className="wrapper">
      <form className="auth" onSubmit={createUser}>
        <div>
          <label htmlFor="email" className="auth-label">
            Email:{' '}
          </label>
          <input
            id="email"
            className="auth-input"
            type="email"
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div className="mt-2">
          <label htmlFor="password" className="auth-label">
            Password:{' '}
          </label>
          <input
            id="password"
            className="auth-input"
            type="password"
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <button className="auth-btn" type="submit">
          SignUp
        </button>
      </form>
      <Link href="/login">
        <a className="auth-link">Login</a>
      </Link>
    </div>
  )
}

export default SignUp

完成形リポジトリ

こちらに完成形のリポジトリを載せています。参考になれば幸いです。
https://github.com/k-logic24/auth-firebase-react-template

(追記)Firebase SDK v9ベータ版が出ている

今回紹介した方法は従来のv8の方法ですが、現在はv9が出ています。
v9では最新のビルドツールを使用しているので、SDKのサイズを最小化できるそうです。ツリーシェイクが可能になったということですね。

気になった方は以下の公式リンクから内容を確認してみてください。
https://firebase.google.com/docs/web/learn-more?hl=ja#modular-version

運営について

Natural Tearoomはシステム開発会社フロントエンドエンジニアがんちゃんが運営するメディアです。
フロントエンド技術を中心に発信しています。

· プライバシーポリシー

SNS

© 2021 天然珈琲店