😣

Next14 ページ移動の検知を行う方法

2024/05/16に公開1

行いたい実装

Next.js 14でページ移動を検知してDialogを表示させたい
サイドバーなどが、layoutを使って実装しているため、page.tsxからpropsを渡すことができない。

前提

Next14では、App Routerの導入やgetServerSideProps関数などを代表する様々なサーバーサイド処理向け関数の廃止や非推奨化になり、今までのNext.jsによる開発方法が使えなくなっています。
(https://nextjs.org/docs/app/api-reference/functions/use-router#migrating-from-nextrouter:~:text=change the response.-,Migrating from,View the full migration guide.,-Examples)

Next.js 13以前は、パスの管理やページ遷移に関わるイベントの管理をnext/routerのuseRouterが行っていましたが、Next13からは、パスの管理などをnext/navigationのusePathnameやuseSearchParamsで行うようになっています。

next/routerが非推奨になってしまったんです。

このnext/navigationのusePathnameやuseSearchParamsは現在のパス名とそれに付随するクエリパラメータの値を取得するための関数で、URLの変更を検知するようなイベントを持っていないんです。

そこで、ユーザーがアプリケーション内のリンクをクリックした際に、確認ダイアログを表示する方法をここで説明していきたいと思います。

ざっくりとした流れの説明

ページ内のすべてのaタグに対して、Dialogを表示するためのクリックイベントを追加する処理を実行する。

aタグの処理を止める

aタグをクリックすると、クリックイベントが発動し、Dialogが表示される。

DialogのクリックイベントにDialogを非表示にするイベントを付与する。または、任意の処理を組み込む。

実際のコードの説明

全体像

page.tsx
import { useParams, useSearchParams } from 'next/navigation'

const params = useParams()
const searchParams = useSearchParams()
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false) 
const links = document.getElementsByTagName('a')

useEffect(() => {
    setIsDialogOpen(false)

    for (let i = 0; i < links.length; i++) {
        links[i].addEventListener('click', (event: MouseEvent) =>
          handleUnansweredLinkClick(event, i)
        )
        
    return () => {
      for (let i = 0; i < links.length; i++) {
        links[i].removeEventListener('click', (event: MouseEvent) =>
          handleUnansweredLinkClick(event, i)
        )
      }
    }
  }, [pathname, searchParams])

  const handleUnansweredLinkClick = (event: MouseEvent, i: number): void => {
    event.preventDefault()
    setIsConfirmDialogOpen(true)

  }

  const closeDialog () => {
    setIsDialogOpen(false)
  }

   return (
    <>
    {isDialogOpen &&
      {/* 表示したいdialogコンポーネント  closeDialog関数を渡して閉じる機能を実装する。(closeDialogをPropsで渡す) */}
    }
    </>
  )

こちらのコードが全てです.

細かく説明

宣言について

  • useParamsとuseSearchParamsを使用してレンダリングの制御を行います。

  • isDialogOpenはDialogを出すかどうかを判断するために状態管理です。

  • linksは、page内のaタグの情報を全て持っています。型は、HTMLCollectionになります。

HTMLCollectionとは?(https://developer.mozilla.org/ja/docs/Web/API/HTMLCollection)

page.tsx
import { useParams, useSearchParams } from 'next/navigation'

const params = useParams()
const searchParams = useSearchParams()
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false) 
const links = document.getElementsByTagName('a')

useEffectについて

  • setIsDialogOpen(false) を呼び出して、isDialogOpen ステート変数を false に設定しています.これにより、画面遷移前にダイアログを閉じることが保証されます。

  • links という変数を使用して、現在のページ内の全てのリンク要素(aタグ)を取得します。links は HTMLCollection というデータ構造で、配列のように要素にアクセスできます。

  • ループを使用して、links 内の各リンク要素に対して、クリックイベントをリッスンする処理を設定しています。これは、ユーザーがこれらのリンクをクリックしたときに特定のアクションを実行できるようにするためのものです。

  • return () => { ... } ブロックは、この useEffect ブロックがクリーンアップされる際に呼び出される関数を定義しています。通常、useEffect 内でリスナーを設定する場合、それらのリスナーを解除するためにこのクリーンアップ関数が使用です。この場合、for ループを使用してすべてのリンク要素のクリックリスナーを削除します。
page.tsx
useEffect(() => {
    setIsDialogOpen(false)

    for (let i = 0; i < links.length; i++) {
        links[i].addEventListener('click', (event: MouseEvent) =>
          handleUnansweredLinkClick(event)
        )
        
    return () => {
      for (let i = 0; i < links.length; i++) {
        links[i].removeEventListener('click', (event: MouseEvent) =>
          handleUnansweredLinkClick(event)
        )
      }
    }
  }, [pathname, searchParams])

aタグに付与されるアクションについて

  • event.preventDefault():この行は、デフォルトのブラウザのクリックアクションをキャンセルし、通常のページ遷移を防ぎます。つまり、リンクをクリックしても、通常のリンクが実行する遷移アクションは実行されません。

  • setIsConfirmDialogOpen(true):この行は、Reactのステート変数である isConfirmDialogOpen の値を true に設定します。これにより、ダイアログを開くための条件が満たされます。
page.tsx
  const handleUnansweredLinkClick = (event): void => {
    event.preventDefault()
    setIsConfirmDialogOpen(true)

  }

表示について

  • closeDialog: isDialogOpenの値をfalseにすることでDialogを非表示にします。

  • closeDialogをPropsでDialogコンポーネントに渡すことでコンポーネントから制御できるようになります。
page.tsx
  const closeDialog () => {
    setIsDialogOpen(false)
  }

   return (
    <>
    {isDialogOpen &&
      {/* 表示したいdialogコンポーネント  closeDialog関数を渡して閉じる機能を実装する。(closeDialogをPropsで渡す) */}
    }
    </>

まとめ

Next.js 13では、ページの移動を検知してダイアログを表示する方法が変わりました。
useEffect フックを使用してリンクのクリックイベントを管理することでページ移動の前にDialogを表示することができます。

今回は、Next.js 13でページ移動を検知してDialogを表示させる方法をご紹介しました。

Discussion

Honey32Honey32

失礼します。この方法だと、ステートの変化等によって遅れてから出現した a タグに対してイベントハンドラが設置できない問題があると思います。

useEffect は React のステートの変更に反応しますが、DOM 自体が書き換わることを検知することが不可能だからです。

おそらく、Event Delegation のパターンに従って実装すると解決すると思います。

https://zenn.dev/benjuwan/articles/6f6db0a07ba508

useEffect(() => {
  if (typeof window === "undefined") return;

  // このように一時変数に関数を保持する必要があります。
  // そうしないと、removeEventListener は失敗して、どんどん新しいイベントハンドラが増えていきます。
  const handler = (e: MouseEvent) => {
    const href = e.target.closest("a")?.href;
    if(!href) return;
    setDialogOpen(true);
    setDialogUrl(href); // dialog の描画のための url を保持するステートを更新する
  }

  window.addEventListener("click", handler);
  return () => {
    window.removeEventListener("click", handler);
  }
}, []); //「a タグのクリックへの割り込み」だけで十分なので、pathname 等を監視する必要なし。