SupabaseとNext.jsでPrivateRouteのリダイレクト処理をしてみる。
どうも。
今日はSupabaseを使用していて、ログイン状態の場合にのみアクセスできるページにリダイレクトしたい場合のページ処理について考えていました。
初歩的なポイントで少し詰まった箇所があったので、それについてメモしておきたいと思います。
リダイレクト処理にはいくつかのポイントがありますので、以下にまとめてみました。
が、個人的には理解しづらい点もあると感じています。
今回の結論を先に述べると、リダイレクトがうまく機能しないタイミングが発生していた原因は、ログイン判定に使用するuserオブジェクトの初期値にnullを設定していたことでした。
今回はRecoilを使用してStateを管理しています。
// stores/index.ts
import { Session, User } from "@supabase/supabase-js";
import { atom } from "recoil";
import { supabase } from "../services/client";
export const userState = atom<User | null>({
key: 'atom_user',
default: null, // ここの初期値がnullだったせいでした
});
userオブジェクトを格納するためのStateをRecoilで準備しました。
このStateは、常に描画される上位のコンポーネントまたは必ず描画されるコンポーネント内でHooksとして使用し、値を判定していきます。
次に、判定用のAuthComponentを作成し、その中で<Component />をラップするようにします。
以下のような形になります。
// _app.tsx
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import { RecoilRoot } from 'recoil'
import Auth from '../components/auth'
export default function Index({ Component, pageProps }: AppProps) {
return (
<RecoilRoot>
<Auth>
<Component {...pageProps} />
</Auth>
</RecoilRoot>
)
}
では、実際にAuthComponentに処理を記述していきましょう。
import { Session, User } from "@supabase/supabase-js";
import { useRouter } from "next/router";
import { useEffect } from "react"
import { useRecoilState } from "recoil";
import { supabase } from "../services/client"
import { userState } from "../stores";
export const useAuth = () => {
const router = useRouter()
const [user, setUser] = useRecoilState<User | null>(userState);
const isSignPage = router.pathname === '/signin' || router.pathname === '/signup'
const isSignCompletePage = router.pathname === '/signup/complete'
useEffect(() => {
setUser(supabase.auth.user() ?? null)
// ログイン、非ログイン時のrouteを分岐する
branchingRoute(user)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// routeを分岐する
const branchingRoute = (user: User | null) => {
// ログインしていない場合
if (!user) {
if (router.pathname === '/signin' || router.pathname === '/signup') {
router.replace(router.pathname)
} else {
router.replace('/')
}
}
// ログインしている場合
if (user) {
if (router.pathname === '/signin' || router.pathname === '/signup') {
router.replace('/')
}
}
}
return {
user,
session
}
}
やっていることは単純で、useEffectの中でユーザーの情報を格納しています。
その格納したユーザー情報を利用して、Routeを切り分けてリダイレクト処理を行っています。
router.push ではなく router.replace を使用しているのは、ログイン後に直接URLを入力してページにアクセスした場合、リダイレクトさせたいページが履歴に残ってしまうためです。
この問題を回避するため、履歴を上書きし、ブラウザの戻る操作でリダイレクト先のページに戻れないようにしています。
ここではSignInやSignOutページにおいて、ログイン時には/index にリダイレクトするように設定していますが、実際に動作させると一瞬だけSignInやSignOutページが表示されてからリダイレクトされるという挙動になるかもしれません。
この挙動の理由は、useStateで設定した初期値のnullが、userの情報の取得が完了する前に一度だけレンダリングされるためです。
その後、useEffect内で正しいユーザー情報が格納されるとリダイレクト処理が行われます。
状態の変遷としては、最初の状態ではuserの値がnullでSignページが描画されます。
その後、user情報が入ったタイミングでリダイレクトが行われます。
この挙動は「初期値とComponentのマウントのタイミング」によって一瞬表示されてしまうということです。
では実際に初期値を変更してみましょう。
// stores/index.ts
import { Session, User } from "@supabase/supabase-js";
import { atom } from "recoil";
import { supabase } from "../services/client";
export const userState = atom<User | null>({
key: 'atom_user',
default: supabase.auth.user(), // ここの初期値をnullから変更
});
初期値をnullからuser情報を取得した値に変更することで、初期のnull状態がなくなりました。その結果、一瞬だけ表示される挙動もなくなりました。
意外とハマるポイントだったので、ここにメモしておきます。
それでは。