Next.jsでページ共通の処理をする(useEffectを使う例)
Nuxt.jsだとmiddlewareという機能を使うことで、ページの遷移ごとに特定の処理をはさむことができます。
Next.jsで全ページで特定の処理をはさむためには、Appコンポーネント(_app.tsx
)にその処理を書くことになります。
Next.jsの_app.tsxに全ページ共通の処理を書く
参考:Next.jsの_app.tsxのカスタマイズ(TypeScript)
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
を中断すれば良いでしょう。
function MyApp({ Component, pageProps, router }: AppProps) {
useEffect(() => {
+ if(router.pathname === "/login") return; // pathnameが"/login"の場合には処理を行わない
// ここに処理を書く
},[router.pathname])
return <Component {...pageProps} />
}
特定のページでのみ処理を行うには
除きたいページが多く、必要になったページでのみ処理を行いたい場合には、カスタムフックを作ると使いまわしやすくて良いかもしれません。カスタムフックは_app.tsx
ではなく、必要なページでのみ読み込みます。
↓ こんな感じでカスタムフックを作って…
import { useEffect } from 'react';
export function useRequireLogin() {
useEffect(()=>{
// ここに処理
},[])
}
↓ 必要なページで読み込みます。
import { NextPage } from 'next'
+ import { useRequireLogin } from "./hooks/useRequireLogin"
const Page: NextPage = () => {
+ useRequireLogin();
return (<div>...ページの内容...</div>)
}
これでカスタムフックが読み込まれたページで処理が行われるようになります。
未ログインならリダイレクトするサンプル
よくありそうな「ログインの有無によって、リダイレクトする」というケースについて考えてみます。ここで紹介するのはあくまでもひとつの実装例です。他にもやり方は色々とあることをご理解ください。
前提
- 個人的な好みにより、認証が必要なリクエストはクライアントサイドから行います。
-
_app.tsx
内のgetServerProps
やgetInitialProps
でcookie
を受け取って、それをAPIサーバーへのリクエストにも付与して…ということも可能ですが、それをやるとせっかくキャッシュしてCDN配信できる静的なページでもgetServerProps
orgetInitialProps
が実行されてしまいます。 - 認証が必要なリクエストはすべてブラウザから行う(=SSRしない)と決めておけば、セキュリティ的な事故をしづらく、気が楽です。
-
- すでにログイン処理は出来ているものとします。あくまでもログイン情報をクライアントのアプリ上のグローバルなステートとしてどう管理するかという例を紹介します。
- このサンプルではグローバルステートの管理にはRecoilを使います。とはいえ、一部の処理を除けばRecoil以外でも同じように書けると思います。
1. アプリのマウント時にログインユーザーの情報を取得して、グローバルステートにセット
ログインユーザーの情報をRecoilにもたせるために、少し準備します。ここはRecoil特有の部分なので詳しくはドキュメントを読んでください。
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引数は空([]
)にします。
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. ログインユーザー情報にどこからでもアクセスできるようにカスタムフックを作る
どのコンポーネントからもログイン有無やログインユーザーの情報を楽に取得できるようにカスタムフックを作ります。
import { useRecoilValue } from 'recoil';
import { currentUserState } from 'states/currentUser'
export function useCurrentUser() {
const currentUser = useRecoilValue(currentUserState); // グローバルステートからcurrentUserを取り出す
const isAuthChecking = currentUser === undefined; // ログイン情報を取得中かどうか
return {
currentUser,
isAuthChecking
};
}
これでコンポーネントから以下のようにログインユーザー情報にアクセスできます。
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. ログインが必要なページ用のカスタムフックを作る
もう少し進んで「ログインしていない場合はログインページへリダイレクトする」カスタムフックを作ります。カスタムフックをさらにカスタムフックから呼ぶのがあまり気持ちよくはありませんが・・・。
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])
}
↓ このカスタムフックをログインが必要なページで読み込みます。
import { NextPage } from 'next'
+ import { useRequireLogin } from "./hooks/useRequireLogin"
const Page: NextPage = () => {
+ useRequireLogin();
return (<div>...ページの内容...</div>)
}
これでログインしていない場合は/login
へリダイレクトされます。もしリダイレクト前に一瞬ページが表示されるのが嫌であれば、ログイン確認中かどうかのフラグ(authChecking
)を見て、ローディングアイコンを表示したりすれば良いと思います。
以上、Next.jsでページ共通の処理を行う一例の紹介でした。参考になれば幸いです。
Discussion
1. アプリのマウント時にログインユーザーの情報を取得して、グローバルステートにセット の次のコードについて質問させてください!
自分はいまNext.jsとRecoilを勉強中なんですが、間違っていたらすいません。
自分が理解している範囲では、Recoilを使うには次のように
RecoilRoot
で囲う必要があり、また
useSetRecoilState
を呼ぶことはRecoilRoot
で囲った子コンポーネントでしかできないと思うんですよ。上記はどうやるんでしょうか?
自分の理解が間違っていたらぜひ教えてください。
よろしくお願いします!
Zenn最高です!
<RecoilRoot>
、入れ忘れてました!修正しました。ご指摘ありがとうございました。
私もこれが気になりまして、
<RecoilRoot>
がある_app.tsx
でsetCurrentUser
すると、Error: This component must be used inside a <RecoilRoot> component.
というエラーが出るので、 上記のコードでは、
_app.tsx
内で stateを更新することはできない気がしますが、どうすればいいでしょうか??あ、そういうことですね。失礼しました。
useEffect
部分(useSetRecoilState
を行う部分)を別コンポーネントに切り出せばOKだと思います。上述のコードもその形に修正しました。ありがとうございます!コードめちゃくちゃ参考になります!
あ、書き忘れてましたが、Zenn最高です!!
ご指摘ありがとうございました! && ありがとうございます!
ご回答ありがとうございます!!!!
最高です!!!!!