Next.jsでリダイレクト(Nuxtでいうところのmiddleware)を実装するには
ログイン状態か否かでlogin画面のリダイレクトするかしないかみたいな設定を各pageごとに定義するのではなく_app.tsxでまとめて定義したい
CSRとSSRで異なる設定方法が必要そう
router.beforPopState的なのでリダイレクトの設定ができる感じか
router.beforePopState(({ url, as, options }) => {
// ログイン画面とエラー画面遷移時のみ認証チェックを行わない
if (url !== '/login' && url !== '/_error') {
if (typeof cookies.auth === 'undefined') {
// CSR用リダイレクト処理
window.location.href = '/login';
return false;
}
}
return true;
});
この部分だけ別ファイル化したいな
router.events.on('routeChangeStart', (url) => {
// eslint-disable-next-line
console.log('routeChangeStart', { url })
})
だとrouter.pushとかは感知してくれるけど
url直叩きだと感知されない
Redirectコンポーネントの実装
next js protected routes
で欲しい情報出てきそう
zennでの実装
ある条件化ではrouter.pushするだけのカスタムフック作るの良さげな感じ
Zennでやってる方法をざっくりと書いてみました。SSR未対応ですが参考になれば!
redux-persist
を使ってる場合の問題点
おそらくrouter.push()で画面遷移した場合はstoreは初期値→local storageから取得になる
router.push()で画面移動した際に
下記のようにリダイレクトをカスタムフックで実装し(local storageの値で判定させている場合)
useEffectでそのstoreの値をみている場合は初期値→local storageの値でuseEffectは二回実行される
export const useRequireName = (): void => {
// local storageから値を取得するhooks
const { user } = useUserStore()
const router = useRouter()
useEffect(() => {
// NOTE: user情報を取得しているのにnameがない場合はリダイレクト
if (user.isFetched && user.name === null) {
router.push('/store')
}
}, [user.name, user.isFetched])
}
初回時はuserにはstoreの初期値しか入ってないのでstore内でisFetchedなどuserを取得したかどうかの状態を保持しておく必要がある
遷移先の画面が一瞬表示される問題は
isFetched(ユーザー情報を保持したかどうか)を元にローディングを表示させるようにする
const RedirectPage: NextPage = () => {
useRequireName()
const { user } = useUserStore()
const [isFetch, setIsFetch] = useState<boolean>()
useEffect(() => {
setIsFetch(user.isFetched)
}, [user.isFetched])
// ローディング
if (!isFetch) return <h1 style={{ color: 'red' }}>ローディング中</h1>
// ....
}
context api使ったauth providerの実装例
providerでrouterのeventを監視する
_app.tsx
でauth用のcontextを呼ぶ
import 'styles/index.css'
import { AuthProvider } from 'contexts/auth'
import { NextPage } from 'next'
import { AppProps } from 'next/app'
const MyApp: NextPage<AppProps> = ({ Component, pageProps }: AppProps) => {
return (
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
)
}
export default MyApp
export const AuthProvider: NextPage = ({ children }) => {
const [user, setUser] = useState<UserContext>({
name: null,
token: null,
loading: false,
})
const { pathname, events } = useRouter()
useEffect(() => {
// NOTE: local storageに格納されている値をみる
// なければapiから取得
setUser({
name: null,
token: null,
loading: true,
})
const fetchUser = async () => {
await sleep(1000)
setUser({
name: 'hada',
// NOTE: ログインの状態を試したいならこっち
// token: 'hada token',
// NOTE: 未ログインの状態を試したいならこっち
token: null,
loading: false,
})
}
fetchUser()
}, [pathname])
useEffect(() => {
const handleRouteChange = (url: string) => {
if (url !== '/login' && user.token === null) {
window.location.href = '/login'
}
// eslint-disable-next-line
console.log('routeChangeイベントやでぇ', { url, pathname })
}
events.on('routeChangeStart', handleRouteChange)
return () => {
events.off('routeChangeStart', handleRouteChange)
}
}, [user])
return <AuthContext.Provider value={user}>{children}</AuthContext.Provider>
}
pathname
が変更されればuser情報をfetchし、userがupdateされたら
tokenとpathをチェックし、tokenがnullの場合はloginへリダイレクトする実装
挙動
ユーザーが未ログインの時にボタン経由で画面遷移する場合
url直接入力して画面遷移した場合
url直入力した時にevents.on('routeChangeStart', handleRouteChange)
が動かないので画面遷移できてしまう
カスタムフックでリダイレクト実装する
AuthProviderでpathnameが変わったらuserをfetchするとこまで同じ
リダイレクトするカスタムフックの実装
import { useAuth, UserContext } from 'contexts/auth'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
type UseUserParams = {
redirectIfFound?: boolean
redirectIfLoggedIn?: boolean
}
export const useUser = ({
redirectIfFound = false,
redirectIfLoggedIn = false,
}: UseUserParams): UserContext => {
const user = useAuth()
const { push } = useRouter()
useEffect(() => {
if (user.loading) return
if (user.token === null && redirectIfFound) {
push('/login')
return
}
if (user.token && redirectIfLoggedIn) {
push('/')
return
}
}, [user])
return user
}
ログイン後に見れるpage側の実装
const IndexPage = () => {
const user = useUser({ redirectIfFound: true })
return user.loading ? (
<h1>ローディング中だよ</h1>
) : (
<div>ログインしたら見れるコンテンツ</div>
)
}
export default IndexPage
ログインページの実装
const LoginPage: NextPage = () => {
const user = useUser({
redirectIfLoggedIn: true,
})
return user.loading ? (
<p>ローディング中だよ</p>
) : (
<div>ログインフォーム</div>
)
}
export default LoginPage
挙動
ユーザーが未ログインの時にボタン経由で画面遷移する場合
url直接入力して画面遷移する場合
それっぽい挙動をするけど、useEffectがレンダリング後に呼ばれるので、若干ページが見える
ローディング中だよ、しか見えないはずなんだけどもな・・