Next.jsでページ共通の処理をする(useEffectを使う例)

公開:2021/01/25
更新:2021/01/26
6 min読了の目安(約6200字TECH技術記事 7

Nuxt.jsだとmiddlewareという機能を使うことで、ページの遷移ごとに特定の処理をはさむことができます。

Next.jsで全ページで特定の処理をはさむためには、Appコンポーネント(_app.tsx)にその処理を書くことになります。

Next.jsの_app.tsxに全ページ共通の処理を書く

参考:Next.jsの_app.tsxのカスタマイズ(TypeScript)

_app.tsx
 import type { AppProps } from 'next/app'
 import { useEffect } from 'react';

 function MyApp({ Component, pageProps, router }: AppProps) {

+  useEffect(() => {
+	  // ここに全ページ共通で行う処理
+  },[router.pathname])

   return <Component {...pageProps} />
 }

 export default MyApp
  • useEffectの第2引数を空([])にすると、初回のみ(クライアントでアプリがマウントされたとき)のみ実行されます。
  • 第2引数に、propsから受け取ったrouter.pathnameを指定すると、ページの遷移ごとに実行されるようになります。

Next.jsのrouterオブジェクト内のpathname/login/users/[username]などの文字列となっています。
queryが変わっただけの場合にも処理を行うにはpathnameの代わりにasPathを使います。asPath/users/foo?bar=1のような実際のURLに含まれるパスとなっています。

ひとつ注意したいのが、useEffectがコンポーネントのマウント後に実行されるという点です。例えばリダイレクトの処理を書く場合、一瞬そのページが表示されることがあります。

特定のページのみ処理を除くには

なかには特定のページでのみ処理を除きたいこともあると思います。除きたいページが数ページであれば、pathnameに応じてuseEffectを中断すれば良いでしょう。

_app.tsx
 function MyApp({ Component, pageProps, router }: AppProps) {

   useEffect(() => {
+     if(router.pathname === "/login") return; // pathnameが"/login"の場合には処理を行わない
      // ここに処理を書く
   },[router.pathname])

  return <Component {...pageProps} />
 }

特定のページでのみ処理を行うには

除きたいページが多く、必要になったページでのみ処理を行いたい場合には、カスタムフックを作ると使いまわしやすくて良いかもしれません。カスタムフックは_app.tsxではなく、必要なページでのみ読み込みます。

↓ こんな感じでカスタムフックを作って…

hooks/useRequireLogin.tsx
import { useEffect } from 'react';

export function useRequireLogin() {
  useEffect(()=>{
	  // ここに処理
  },[])
}

↓ 必要なページで読み込みます。

pages/users/[username].tsx
  import { NextPage } from 'next'
+ import { useRequireLogin } from "./hooks/useRequireLogin"

  const Page: NextPage = () => {

+   useRequireLogin();
  
    return (<div>...ページの内容...</div>)
  }

これでカスタムフックが読み込まれたページで処理が行われるようになります。

未ログインならリダイレクトするサンプル

よくありそうな「ログインの有無によって、リダイレクトする」というケースについて考えてみます。ここで紹介するのはあくまでもひとつの実装例です。他にもやり方は色々とあることをご理解ください。

前提

  • 個人的な好みにより、認証が必要なリクエストはクライアントサイドから行います。
    • _app.tsx内のgetServerPropsgetInitialPropscookieを受け取って、それをAPIサーバーへのリクエストにも付与して…ということも可能ですが、それをやるとせっかくキャッシュしてCDN配信できる静的なページでもgetServerProps or getInitialPropsが実行されてしまいます。
    • 認証が必要なリクエストはすべてブラウザから行う(=SSRしない)と決めておけば、セキュリティ的な事故をしづらく、気が楽です。
  • すでにログイン処理は出来ているものとします。あくまでもログイン情報をクライアントのアプリ上のグローバルなステートとしてどう管理するかという例を紹介します。
  • このサンプルではグローバルステートの管理にはRecoilを使います。とはいえ、一部の処理を除けばRecoil以外でも同じように書けると思います。

1. アプリのマウント時にログインユーザーの情報を取得して、グローバルステートにセット

ログインユーザーの情報をRecoilにもたせるために、少し準備します。ここはRecoil特有の部分なので詳しくはドキュメントを読んでください。

states/currentUser.tsx
import { atom } from 'recoil';
import { CurrentUser } from '../types/user'; // ログインユーザーの型定義

// undefined : まだログイン確認が完了していない状態とする
// null      : ログイン確認をした結果、ログインしていなかった状態とする
export const currentUserState = atom<undefined | null | CurrentUser>({
  key: 'CurrentUser',
  default: undefined,
});

そして、_app.tsxでアプリがマウントされたタイミングで、サーバーへリクエストを送ってログインユーザーの情報を取得します。当たり前ですが、一度だけ取得ができれば良いので、useEffectの第2引数は空([])にします。

_app.tsx
import type { AppProps } from 'next/app'
import { useEffect } from 'react';
import { useSetRecoilState, RecoilRoot } from 'recoil';
import { currentUserState } from '../states/user';
import { fetchCurrentUser } from '../requests/currentUser';

// useSetRecoilStateは <RecoilRoot> の子コンポーネントで呼び出さないとエラーになる
// => マウント後に処理を行うだけのコンポーネントを作り、MyAppから呼び出す
function AppInit() {
  // グローバルステートにユーザー情報をセットするためのもの
  const setCurrentUser = useSetRecoilState(currentUserState);

  useEffect(() => {
    (async function () {
      try {
        const { currentUser } = await fetchCurrentUser(); // サーバーへのリクエスト(未ログインの場合は401等を返すものとする)
	// ログインユーザーの情報が取得できたのでグローバルステートにセット
        setCurrentUser(currentUser);
      } catch {
        // 未ログイン(未ログイン時のリダイレクト処理などをここに書いても良いかも)
        setCurrentUser(null);
      }
    })();
  },[])
  
  return null;
}

function MyApp({ Component, pageProps, router }: AppProps) {
  return (
    <RecoilRoot>
      <Component {...pageProps} />
      <AppInit />
    </RecoilRoot>
  );
}

export default MyApp

2. ログインユーザー情報にどこからでもアクセスできるようにカスタムフックを作る

どのコンポーネントからもログイン有無やログインユーザーの情報を楽に取得できるようにカスタムフックを作ります。

hooks/useCurrentUser.tsx
import { useRecoilValue } from 'recoil';
import { currentUserState } from 'states/currentUser'

export function useCurrentUser() {
  const currentUser = useRecoilValue(currentUserState); // グローバルステートからcurrentUserを取り出す
  const isAuthChecking = currentUser === undefined; // ログイン情報を取得中かどうか

  return {
    currentUser,
    isAuthChecking
  };
}

これでコンポーネントから以下のようにログインユーザー情報にアクセスできます。

pages/users/[username].tsx
import { NextPage } from 'next'
import { useCurrentUser } from "./hooks/useCurrentUser"

const Page: NextPage = () => {
  const { authChecking, currentUser } = useCurrentUser();
  
  if(authChecking) return (<div>ログイン情報を確認中…</div>);
  
  if(!currentUser) return (<div>ログインしていません</div>);
  
  return (<div>あなたのユーザー名は{currentUser.name}です</div>);
}

3. ログインが必要なページ用のカスタムフックを作る

もう少し進んで「ログインしていない場合はログインページへリダイレクトする」カスタムフックを作ります。カスタムフックをさらにカスタムフックから呼ぶのがあまり気持ちよくはありませんが・・・。

hooks/useRequireLogin.tsx
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useCurrentUser } from "./useCurrentUser"

export function useRequireLogin() {
  const { authChecking, currentUser } = useCurrentUser();
  const router = useRouter();
  
  userEffect(()=>{
    if(authChecking) return; // まだ確認中
    if(!currentUser) router.push("/login"); // 未ログインだったのでリダイレクト
  },[authChecking, currentUser])
}

↓ このカスタムフックをログインが必要なページで読み込みます。

pages/users/[username].tsx
  import { NextPage } from 'next'
+ import { useRequireLogin } from "./hooks/useRequireLogin"

  const Page: NextPage = () => {

+   useRequireLogin();
  
    return (<div>...ページの内容...</div>)
  }

これでログインしていない場合は/loginへリダイレクトされます。もしリダイレクト前に一瞬ページが表示されるのが嫌であれば、ログイン確認中かどうかのフラグ(authChecking)を見て、ローディングアイコンを表示したりすれば良いと思います。


以上、Next.jsでページ共通の処理を行う一例の紹介でした。参考になれば幸いです。