😺

Recoil で Next.js x Firebase Twitter auth を実装

7 min read 1

Next.js x TypeScript x Firebase Authentication で Twitterログインを実装するケースは今後増えそうですが、Firebase公式ドキュメントを見てもサクッと実装できなかったのでZennにします。

Recoil, Atom, State まわりの理解が正直自信ないので、「意味わからないことしてる」「もっとスマートな書き方がある」などあればコメントでご指摘いただけると幸いですm(_ _)m

環境 & 注意

  • TypeScript
  • Node.js 16
  • M1 Mac...

Next.js(SSR)なのでFirebaesのinitまわりのコードがサーバーサイドで走ったりクライアントサイドで走ったりします。またFirebaseのinitializeは1回しか行ってはいけない(1度ビルドしてinitializeされてれば「Firebase App named '[DEFAULT]' already exists」と怒られる)。筆者の理解と実験が正しければ以下のようにすれば完全に棲み分けできるはず。

firebase.tsx
if (typeof window === "undefined") {
    console.log('run on server-side.')
} else {
    console.log('run on client-side')
     if (!firebase.apps.length) { // if (firebase.apps.length === 0) でもいいはず
       firebaseApp = firebase.initializeApp(config);
    } else {
      firebaseApp = firebase.app();
    }
}

ただ多くの先達たちは以下で完結してました。

firebase.tsx
!firebase.apps.length ? firebase.initializeApp(config) : firebase.app();

ベースとなる先輩の情報

まずTwitterログインをするだけなら@mochiさんの下記のZennで解決します。

https://zenn.dev/mochi/articles/33f452bb53f2d6aee956

しかし当方が@dalaさんの下記の本(有料)に習って開発している関係でRecoilを使ってやりました。(Reactの状態管理においてRecoilはまだ実験段階の仕組みで本番利用は怖かったですが、Reduxを抜く人気が出てきているらしく採用しました)

https://zenn.dev/dala/books/nextjs-firebase-service/viewer/recoil

基礎知識

  • stateは本来ローカルなもので各コンポーネント自身が内包しているもの
  • コンポーネントにstateを持たせるためには、本来は関数の形ではできずClassとして定義してやる必要がある https://ja.reactjs.org/docs/state-and-lifecycle.html
  • しかしHooksを使えばClassを用意せずに関数の形でstateを使える https://ja.reactjs.org/docs/hooks-intro.html
  • useEffectを使えばDOMの更新時に毎回行ってほしいアクションを記述できる https://ja.reactjs.org/docs/hooks-effect.html
  • stateはコンポーネント外からはアクセスできないが、親が子に引数として渡すことはできる
  • 滝のように上から下にしか伝播しないらしい
  • Recoilはstateのツリー間の伝播をせずともどこからでもAtomという箱に入れたstateを使えるようにするものっぽい https://ics.media/entry/210224/

Recoilでの状態管理は実にシンプルで、useRecoilState フックを呼び出すだけです。 useState フックと違う点は、生の値を直接扱うのではなく、Atomというステートオブジェクトを通して値を管理するところです。
https://sbfl.net/blog/2020/05/17/react-experimental-recoil-usage/

コード

Userモデル

User.ts
export interface User {
  uid: string
  providerId: string
  displayName: string
}

firebase.ts

lib/firebase.ts
import firebase from "firebase/app";
import "firebase/auth";

export const config = {
  // 省略
};
!firebase.apps.length ? firebase.initializeApp(config) : firebase.app();

export const auth = firebase.auth();

_app.tsx

RecoilRootで囲んで、firebaseのinitializeコードをimport

pages/_app.tsx
import { RecoilRoot } from 'recoil'
import '../lib/firebase'
import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
  return (
    <RecoilRoot>
      <Component {...pageProps} />
    </RecoilRoot>
  )
}

export default MyApp

index.tsx

hooksをimportしてconst { user } = useAuthentication()。各ページでこれを呼べばいいのか?

pages/index.tsx
import Head from 'next/head'
import styles from '../styles/Home.module.css'
import { useAuthentication, Login, Logout } from '../hooks/authentication'

export default function Home() {
  const { user } = useAuthentication() // ここでHookを呼んでいる

  return (
    <div className={styles.container}>
      <Head>
        // 省略
      </Head>

      <main className={styles.main}>
        <div>
          <button onClick={() => Login()}>ログイン</button>
          <button onClick={() => Logout()}>ログアウト</button>
        </div>

        <div>
          <pre>
            {user
              ? user.displayName + "でログインしています"
              : "ログインしていません"}
          </pre>
        </div>

    </div>
  )
}

authentication.ts

hooks/authentication.ts
import firebase from 'firebase/app'
import { auth } from '../lib/firebase'
import { useEffect } from 'react'
import { atom, useRecoilState } from 'recoil'
import { User } from '../models/User'

const userState = atom<User>({
  key: 'user',
  default: null,
})

// 最新のfirebaseUserをステートとして返す関数
export function useAuthentication() {
  const [user, setUser] = useRecoilState(userState);

  console.log('Start useEffect')

  useEffect(() => {  
    // firebase auth state の購読
    // () => Login() でTwitterログインしたらRecoilStateをsetするための処理
    const unsub = firebase.auth().onAuthStateChanged(function (firebaseUser) {
      if (firebaseUser) {
        console.log('Set user')
        setUser({
          uid: firebaseUser.uid,
          providerId: firebaseUser.providerId,
          displayName: firebaseUser.displayName
        })
      } else {
        // User is signed out.
        setUser(null)
      }
    })
    
    // TwitterのOAuthトークンを取得したい場合のみ(Twitter APIを使って追加情報を取得するなど)
    // SignInWithRedirect()で戻ってきたときにresultを取得する
    firebase.auth()
      .getRedirectResult()
      .then((result) => {
        console.log('result: ', result)
        if (result.credential) {
          /** @type {firebase.auth.OAuthCredential} */
          var credential: firebase.auth.OAuthCredential = result.credential;

          // This gives you a the Twitter OAuth 1.0 Access Token and Secret.
          // You can use these server side with your app's credentials to access the Twitter API.
          var token = credential.accessToken;
          var secret = credential.secret;
          // ...
        }

        // The signed-in user info.
        var user = result.user;
      }).catch((error) => {
        // Handle Errors here.
        var errorCode = error.code;
        var errorMessage = error.message;
        // The email of the user's account used.
        var email = error.email;
        // The firebase.auth.AuthCredential type that was used.
        var credential = error.credential;
        // ...
      });

    // コンポーネント削除時に購読をクリーンアップ
    return () => unsub();
  }, []) // useEffectを1回だけ呼ぶために第2引数に[]を渡す

  return { user }
};

// ここでTwitterログイン
export const Login = () => {
  console.log('Login..')
  const provider = new firebase.auth.TwitterAuthProvider();
  auth
    .signInWithRedirect(provider)
    .then(function (result: any) {
      console.log('Logged in successfully')
      console.log('result: ', result)
      return result;
    })
    .catch(function (error) {
      console.error(error);
    });
};

export const Logout = () => {
  auth
    .signOut()
    .then(() => {
      window.location.reload();
    });
};

まとめ

今回はReacoil x Firebase Twitter authでのログイン部分だけ紹介させていただきました。Recoil x Firebase Authの匿名サインインのケースなどはdalaさんの本などを参考にされてください。

https://zenn.dev/dala/books/nextjs-firebase-service

Discussion

肝心のTwitterログインをしているLogin()が定義されてなかったり、色々とコードがおかしかったので修正しましたm(_ _)m

ログインするとコメントできます