ちょっとハマったので備忘録
ここで紹介する方法以外でベストプラクティス等あればコメントで教えていただけると嬉しいです
Firebase Authentication + SPA でよくある設計
Firebase Authentication を使ったログイン機能付き SPA の実装例をググったときに見かける設計として
「認証が必要なページにアクセスされたときは、ログインしていればそのまま表示し、ログインしていなければログインページへ飛ばす」
というのがよくあるパターンだと思います。例えば React Router を使うと次のような実装が考えられます。
※ React Router ドキュメントのコードをベースにしています https://reactrouter.com/web/example/auth-workflow
const PrivateRoute: React.FC<RouteProps> = ({ children, ...rest }) => {
// getUser は保持されたユーザー情報を取得するための何らかの関数です。実装は省略
// 後で登場する setUser 関数で保持したユーザー情報を取り出します
// state を使ってもいいし、Redux 等のストアを使ってもいいし、Context を使っても構いません
const user = getUser();
return (
<Route
{...rest}
render={({ location }) => {
if (user) {
return children;
} else {
return <Redirect to={{ pathname: 'login', state: { from: location } }} />;
}
}}
/>
);
};
export default function App() {
useEffect(() => {
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// setUser はユーザー情報を保持しておくための何らかの関数です。実装は省略
// state を使ってもいいし、Redux 等のストアを使ってもいいし、Context を使っても構いません
setUser(user);
}
});
}, []);
return (
<>
<Router>
<Link to="/public">ログインが不要なページ</Link>
<Link to="/private">ログインが必要なページ</Link>
<Switch>
<Route path="/login">
<>
<div>ログインページです</div>
{/* Firebase Authentication へのログイン方法は何でも OK ですが */}
{/* ここでは StyledFirebaseAuth を使ってログイン画面を表示します */}
<StyledFirebaseAuth ... />
</>
</Route>
<Route path="/public">
<div>このページは誰でも見ることができます</div>
</Route>
<PrivateRoute path="/private">
<div>このページはログインしたユーザーだけが見ることができます</div>
</PrivateRoute>
</Switch>
</Router>
</>
);
}
この実装で基本的には問題ないです。トップページ /
から /public
に遷移するとログインの有無に関係なくページが表示され、 /private
に遷移するとログインしていれば表示されるし、ログインしていなければログインページへリダイレクトされます
問題点
ページ遷移でアプリケーション内をぐるぐるする分には問題ないのですが、 /private
にいる状態でページをリロードしてみると先程ログインしたのにまたログインページへ飛ばされてしまいます
Firebase Authentication 的にはログイン状態ではあるのですが、 firebase.auth().onAuthStateChanged
は非同期で走るので、 /private
にダイレクトにアクセスすると setUser
がされる前に getUser
が実行されてしまいます
setUser
されてないのでユーザー情報は存在せず、ログインされていない判定になりログインページへ再び飛ばされてしまいます
解決方法
実装を工夫してあげる必要があります
解決方法はシンプルで onAuthStateChanged
が終わってからページを表示するかログインページへ飛ばすかどうかを確定させるアプローチで対処できます
(ここでは React Router を使った場合の解決方法を書いていますが、他のルーターの場合でもミドルウェアを使って似たようなことができると思います)
PrivateRoute
を次のように書き換えてあげます
const PrivateRoute: React.FC<RouteProps> = ({ children, ...rest }) => {
const [authChecked, setAuthChecked] = useState(false);
useEffect(() => {
firebaseAuth.onAuthStateChanged((user) => {
if (user) {
setUser(user);
}
setAuthChecked(true);
});
}, []);
const user = getUser();
return (
<Route
{...rest}
render={({ location }) => {
if (authChecked) {
if (user) {
return children;
} else {
return <Redirect to={{ pathname: 'login', state: { from: location } }} />;
}
} else {
return <></>;
}
}}
/>
);
};
「ログイン状態の確認が終わったか」「ログインしているかどうか」の2段階で分岐することで UX を損なうことなくリロード問題に対処できるようになりました