👏

RecoilでFirebase Authを使う

2021/07/06に公開

Recoilを使ってみたいので、Firebase Authで使ってみました。

なるべく複雑なことをしないようにRecoilでFirebaseのUserオブジェクトを保持して、認証状態でルーティングするだけのシンプルなものです。

使うもの

  • React
  • Recoil
  • React Router Dom
  • Firebase

ディレクトリ

  • src/
    • index.tsx
    • App.tsx
    • firebase.js
    • hooks/Auth.tsx
    • pages/Login.tsx

1. Atomの設定

/hooks/Auth.tsx
import { atom } from 'recoil';
import firebase from 'firebase';

type AuthState = firebase.User| null;

const authState = atom<AuthState>({
  key: 'authState',
  default: null,
  // TypeError: Cannot freezeを回避
  dangerouslyAllowMutability: true,
});

export default authState;

今回はお試しということでカスタムフックを作ったりせずにシンプルにatomだけで書いてみました。Reactのcontextを使う場合と比べてもコード量は少ないと思います。

1-1. TypeError: Cannot freeze

Recoilはストアされたオブジェクトを再帰的にfreeze(deep freeze)しているため、freezeできないオブジェクト(firebaseのUserオブジェクトなど)をストアする場合はTypeError: Cannot freezeとなります。

今回はオブジェクトをfreezeできるように変更できないので。dangerouslyAllowMutability: trueとします。
https://github.com/facebookexperimental/Recoil/issues/406

2. RecoilRootの設定

index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { RecoilRoot } from 'recoil';
import App from './App';

ReactDOM.render(
  <RecoilRoot>
    <App />
  </RecoilRoot>
  document.getElementById('root')
);

Recoilを使うコンポーネントは<RecoilRoot>で囲む必要があるので、index.tsxなどでこのように書いておきます。

3. ログイン

pages/Login.tsx
import React, { useRef } from 'react';
import { useHistory } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import firebase from '../firebase';
import authState from '../hooks/Auth';

const Login = () => {
  const setAuth = useSetRecoilState(authState);
  const history = useHistory();
  const inputEmailRef = useRef<HTMLInputElement>(null);
  const inputPasswordRef = useRef<HTMLInputElement>(null);

  const handleLogin = async () => {
    if (
      inputEmailRef.current?.value !== undefined &&
      inputPasswordRef.current?.value !== undefined
    ) {
      try {
        const auth = await firebase
          .auth()
          .signInWithEmailAndPassword(
            inputEmailRef.current.value,
            inputPasswordRef.current.value
          );

        if (auth.user) {
          setAuth(auth.user);
          history.push('/');
        }
      } catch (e) {
        // handle error
      }
    }
  };

  return (
    <>
      <label htmlFor="email">
        メールアドレス
        <input
          type="text"
          id="email"
          name="email"
          placeholder="メールアドレス"
          ref={inputEmailRef}
        />
      </label>
      <label htmlFor="password">
        パスワード
        <input
          type="password"
          id="password"
          name="password"
          placeholder="パスワード"
          ref={inputPasswordRef}
        />
      </label>

      <button onClick={handleLogin} type="submit">
        ログイン
      </button>
    </>
  );
};

export default Login;

setterを取得するuseSetRecoilState()を使い、ログインできたらFirebase AuthのユーザーオブジェクトをauthStateにセットします。

4. ルーティング

App.tsx
import React, { useEffect } from 'react';
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom';
import firebase from 'firebase';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import Home from './views/Home';
import authState from './hooks/Auth';

const PrivateRoute = ({ ...rest }) => {
  const auth = useRecoilValue(authState);

  return auth ? <Route {...rest} /> : <Redirect to="/login" />;
};

const GuestRoute = ({ ...rest }) => {
  const auth = useRecoilValue(authState);

  return auth ? <Redirect to="/" /> : <Route {...rest} />;
};

const App = () => {
  const setAuth = useSetRecoilState(authState);
  const [isLoading, setIsLoading] = useState<Boolean>(true);

  useEffect(() => {
    const unsubscribe = firebase.auth().onAuthStateChanged((authUser) => {
      if (authUser) {
        setAuth(authUser);
      }
      setIsLoading(false);
    });
    return () => {
      unsubscribe();
    };
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  return (
    <BrowserRouter>
      {isLoading ? (
        <p>Loading..</p>
      ) : (
        <Switch>
          <PrivateRoute exact path="/" component={Home} />
          <GuestRoute path="/login" component={Login} />
        </Switch>
      )}
    </BrowserRouter>
  );
};

export default App;

Firebase Authの認証状態を購読して、ルーティングをします。

4-1. onAuthStateChanged()を待ってからレンダリング

firebase.auth().onAuthStateChanged()で現在のユーザーを確認してからルーティングするために、isLoadingをstateとして使います。とりあえず、今回はisLoading中は<p>Loading...</p>と表示させます。

これをしないとリロード時やログイン状態で/loginにアクセスした時に意図しないリダイレクトがされてしまいます。

4-2. PrivateRoute/GuestRoute

auth認証が通っているかで判定して、Redirectをしています。

https://www.hypertextcandy.com/react-route-guard

4-3. useEffect()

auth認証のオブザーバーonAuthStateChanged()はunsubscribeする関数を返すのでconst unsubscribeに入れておいて、return()でunsubscribeすることでクリーンアップします。

4-4. Warn: React Hook useEffect has a missing dependency

eslintのreact-hooks/exhaustive-depsを有効にしていると、以下のように警告されます。

React Hook useEffect has a missing dependency: 'setAuth'. Either include it or remove the dependency array.

useEffect()で依存配列を指定しないのは、バグが起きやすくなるので非推奨となっています。

マウント時に一度だけ実行したい場合にどうするべきかは、Githubでも議論されていました。

一応、[setAuth]と入れることで回避はできますが、あまり意味がなく余計な混乱を招きそうな気がしたので、今回は// eslint-disable-line react-hooks/exhaustive-depsでルールを無効にしました。

もしよりベターの方法があれば教えてください。

Discussion