Open24

Next.jsでリダイレクト(Nuxtでいうところのmiddleware)を実装するには

nus3nus3

ログイン状態か否かでlogin画面のリダイレクトするかしないかみたいな設定を各pageごとに定義するのではなく_app.tsxでまとめて定義したい

nus3nus3

CSRとSSRで異なる設定方法が必要そう
router.beforPopState的なのでリダイレクトの設定ができる感じか
https://qiita.com/YU-TA-9/items/d7244ffd09bda08ce8ce

router.beforePopState(({ url, as, options }) => {
      // ログイン画面とエラー画面遷移時のみ認証チェックを行わない
      if (url !== '/login' && url !== '/_error') {
        if (typeof cookies.auth === 'undefined') {
          // CSR用リダイレクト処理
          window.location.href = '/login';
          return false;
        }
      }
      return true;
    });

この部分だけ別ファイル化したいな

nus3nus3
    router.events.on('routeChangeStart', (url) => {
      // eslint-disable-next-line
      console.log('routeChangeStart', { url })
    })

だとrouter.pushとかは感知してくれるけど
url直叩きだと感知されない

nus3nus3

next js protected routesで欲しい情報出てきそう

catnosecatnose

Zennでやってる方法をざっくりと書いてみました。SSR未対応ですが参考になれば!

https://zenn.dev/catnose99/articles/2169dae14b58b6

nus3nus3

ぬぉぉぉぉぉ!
めちゃめちゃ参考になります!!
Nuxt.jsのmiddlewareみたいなのをカスタムフックで実装しようと悶々としてたので

ありがとうございまっす!!!

catnosecatnose

お、少しでも参考になったのであれば良かったです!ありがとうございます!

nus3nus3

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])
}
nus3nus3

初回時はuserにはstoreの初期値しか入ってないのでstore内でisFetchedなどuserを取得したかどうかの状態を保持しておく必要がある

nus3nus3

遷移先の画面が一瞬表示される問題は
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>

  // ....
}
nus3nus3

context api使ったauth providerの実装例
https://medium.com/@tafka_labs/auth-redirect-in-nextjs-3a3a524c0a06

nus3nus3

context api使ったauth providerの実装例
で実装してもurl直叩きの時にリダイレクトされない

events.on('routeChangeStart', handleRouteChange)
はurlを直接叩いた場合に発火しない

nus3nus3

動作検証メモ

共有会の時に見せるデモは二つ

nus3nus3

providerでrouterのeventを監視する

_app.tsxでauth用のcontextを呼ぶ

_app.tsx
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
src/contexts/auth.tsx
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へリダイレクトする実装

同じ実装してるところ

nus3nus3

挙動

ユーザーが未ログインの時にボタン経由で画面遷移する場合

url直接入力して画面遷移した場合

url直入力した時にevents.on('routeChangeStart', handleRouteChange)が動かないので画面遷移できてしまう

nus3nus3

カスタムフックでリダイレクト実装する

AuthProviderでpathnameが変わったらuserをfetchするとこまで同じ

リダイレクトするカスタムフックの実装

src/hooks/useUser.ts
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側の実装

src/pages/index.tsx
const IndexPage = () => {
  const user = useUser({ redirectIfFound: true })
  
  return user.loading ? (
    <h1>ローディング中だよ</h1>
  ) : (
     <div>ログインしたら見れるコンテンツ</div>
  )
}

export default IndexPage

ログインページの実装

src/pages/login.tsx
const LoginPage: NextPage = () => {
  const user = useUser({
    redirectIfLoggedIn: true,
  })

  return user.loading ? (
    <p>ローディング中だよ</p>
  ) : (
    <div>ログインフォーム</div>
  )
}

export default LoginPage
nus3nus3

挙動

ユーザーが未ログインの時にボタン経由で画面遷移する場合

url直接入力して画面遷移する場合

nus3nus3

それっぽい挙動をするけど、useEffectがレンダリング後に呼ばれるので、若干ページが見える
ローディング中だよ、しか見えないはずなんだけどもな・・