【Next.js x Firebase】OAuth認証後のonAuthStateChangedのラグを誤魔化したい
この記事の前提
- Next.js
- Firebase(v9)
- 認証機能は
/login
ページに実装する
※実用的な話ではないのでネタとしてお楽しみください
前置き
FirebaseのOAuth認証メソッドの一つであるsignInWithRedirect
を使うと、認証用の別サイトにリダイレクトしてしまう("With Redirect" やからな)。その上、OAuth認証後に戻ってくるのはOAuth認証前のURLだから、認証後にトップページに戻ってくることもできない。
もちろんログインボタンを押したらrouter.push()
でそのままリダイレクトさせる、なんてことも出来ない。しかし、Global Stateと組み合わせればOAuth認証後の自動リダイレクトを間接的に実装することができる。
自動リダイレクト
- 初期値が
null
のGlobal Stateを作ってあげて(ここではRecoilを使っている)、
const User = atom({
key: "user",
default: null
});
-
onAuthStateChanged
のコールバックでユーザー情報を取得し、
useEffect(() => {
onAuthStateChanged(auth, (user) => {
setUser(user);
});
}, [])
- ログインを実装するページで、
user
がfalsyでないならトップにリダイレクトするようにすれば、
const { user } = useUser();
useEffect(() => {
user && router.push("/")
}, [user]);
OAuth認証画面から戻ってきて、DOMマウント後onAuthStateChanged
のコールバックが発火したときに好きなページにリダイレクトさせることができる。結構シンプルやね。
『認証中』画面が欲しい
しかし実はリダイレクトだけの実装には穴があって、DOMのマウントからonAuthStateChanged
のコールバックの発火までラグがあり、その間ログイン画面が剝き出しになってしまう。う~んカッコよくないな~。
これはリダイレクト元のURLに戻ってくるOAuth認証の仕様上どうしようもないので、『リダイレクトするまで認証中です的な画面で覆いたいよな』という考えに至った。
Vercelのリダイレクト画面
それではやっていきましょう。
Authenticating...
Reactにおいて画面切り替えといえばuseState
が真っ先に思い浮かぶが、OAuth認証のためにアンマウントしているせいでuseRouter
同様その手は使えない。
そうなると、『ログインボタンからアクセスしたときにはあって、認証画面から戻ってきたときにはないパラメーター(もしくは逆)』を作る必要がある。そんなん作れるっけ…?
next/link
があるじゃない
ここで「確かnext/link
に見せかけのURLを設定する機能があったような…?」という記憶がおぼろげながら浮かんできた。よし、next/link
をおさらいしてみよう。
<Link
href={{
pathname: "...",
query: {}
}}
prefetch={}
as="..."
// ...その他いろいろ
>
<a>This is Link</a>
</Link>
ここでas
の公式ドキュメントを見てみよう。
as
― Optional decorator for the path that will be shown in the browser URL bar. Before Next.js 9.5.3 this was used for dynamic routes, check our previous docs to see how it worked. Note: when this path differs from the one provided in href the previous href/as behavior is used as shown in the previous docs. (全文)
意訳
as
で指定した文字列はブラウザに表示されるパスになるよ。Next 9.5.3まではDynamic Routesへの遷移で使ってたけど、今はhref
だけで行けるようになったよ。
お?as
でブラウザに表示されるURLを設定できる?ひょっとしてURLからクエリを隠して遷移できるってこと?というわけで試してみる。
as
で隠したクエリは渡せるか
このようなLink
コンポーネントを作ってみる。
<Link
href={{
pathname: "/login",
query: { nullUser: "true" } // 便宜上
}}
as="/login"
>
<a>Login</a>
</Link>
結果@login.tsx
...行けるやん...!!!
JSによる遷移では/login
は{ nullUser: "true" }
を受け取ることができるが、外部からのリダイレクトでアクセスしたときは受け取ることができない。これで「ログインボタンからアクセスしたときは~」が実装できる。
実装
じゃあ実際に作ってみよう。nullUser
の値でLoginView
とAuthenticating
を切り替えている。
const LoginView: VFC = () => {
const login = () => {
signInWithRedirect(auth);
};
return (
//...
);
};
const LoginPage: VFC = () => {
const { push, query } = useRouter()
useEffect(() => {
user && push("/")
}, [user])
const { nullUser }: { nullUser: "true" } = query;
return nullUser ? <LoginView /> : <Authenticating />;
};
export default LoginPage;
無理やりですが
実際に作ってみたやつ
かなり力ずくの方法だとは思うが、Next.jsとFirebaseでOAuth認証後に『認証中』を表示してみた。もちろんこの方法にも穴はあって、/login
でリロードされると強制認証中になってしまう。まあログインページでリロードする人はそんなにいないと思うけど…
Discussion