RecoilでFirebase Authを使う
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の設定
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
とします。
2. RecoilRootの設定
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. ログイン
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. ルーティング
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をしています。
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