📱

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

2021/01/25に公開
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を指定すると、ページの遷移ごとに実行されるようになります。

ひとつ注意したいのが、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でページ共通の処理を行う一例の紹介でした。参考になれば幸いです。

Discussion

Shuhei MatsuokaShuhei Matsuoka

1. アプリのマウント時にログインユーザーの情報を取得して、グローバルステートにセット の次のコードについて質問させてください!

function MyApp({ Component, pageProps, router }: AppProps) {
  // ↓ グローバルステートにユーザー情報をセットするためのもの
  const setCurrentUser = useSetRecoilState(currentUserState);

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

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

自分はいまNext.jsとRecoilを勉強中なんですが、間違っていたらすいません。
自分が理解している範囲では、Recoilを使うには次のようにRecoilRootで囲う必要があり、

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

またuseSetRecoilStateを呼ぶことはRecoilRootで囲った子コンポーネントでしかできないと思うんですよ。

上記はどうやるんでしょうか?
自分の理解が間違っていたらぜひ教えてください。
よろしくお願いします!

Zenn最高です!

catnosecatnose

<RecoilRoot>、入れ忘れてました!
修正しました。ご指摘ありがとうございました。

cotaponcotapon

またuseSetRecoilStateを呼ぶことはRecoilRootで囲った子コンポーネントでしかできないと思うんですよ

私もこれが気になりまして、 <RecoilRoot> がある _app.tsxsetCurrentUser すると、
Error: This component must be used inside a <RecoilRoot> component.
というエラーが出るので、 上記のコードでは、 _app.tsx 内で stateを更新することはできない気がしますが、どうすればいいでしょうか??

catnosecatnose

あ、そういうことですね。失礼しました。
useEffect部分(useSetRecoilStateを行う部分)を別コンポーネントに切り出せばOKだと思います。上述のコードもその形に修正しました。

cotaponcotapon

ありがとうございます!コードめちゃくちゃ参考になります!
あ、書き忘れてましたが、Zenn最高です!!

catnosecatnose

ご指摘ありがとうございました! && ありがとうございます!