Reactにおけるページ遷移について

はじめに

TicketMeでエンジニアをしている坂西です。現在はチケット印刷機能の開発に携わっています。

React や Next.js でのページ遷移は、コンポーネントのライフサイクルに大きな影響を与えます。適切な方法を理解し、使い分けることが重要です。 実際のプロジェクトで発生した、サーバーサイドレンダリングからISRに移行した際に発生したクライアントサイドナビゲーションの問題について説明します。

経緯

当初、すべてのページでSSRが採用されており、ページ遷移はサーバーサイドナビゲーションでした。 利用者が増えるに伴い、負荷が増加したため、一部のページをSSRからISRに移行しました。 この移行によって、一部のページ遷移でクライアントサイドナビゲーションが行われるようになりました。

ただ、Chakra UIのLinkコンポーネントをそのまま使用していたため、フルページリロードが発生しており、当初は特に問題はありませんでした。

しかし、国際化対応(i18n)の実装を進める中で、Next.jsのページ遷移(SPA遷移)が必要になり、Linkコンポーネントに as={NextLink} を設定することになりました。 これにより、ページ遷移時にフルページリロードが行われるのではなく、部分的な更新が行われるようになりました。 その結果、ページ遷移時にコンポーネントが再レンダリングされず、データが更新されないといった不具合が発生しました。

原因

フルページリロードの場合は、すべてのコンポーネントがアンマウントされ、新しいページがロードされる際に再マウントされます。

一方で、クライアントサイドナビゲーションの場合、以前のページのコンポーネントはアンマウントされ、新しいページのコンポーネントがマウントされます。ただし、共通のレイアウトコンポーネント(たとえば、ナビゲーションバーやフッターなど)はアンマウントされず、そのまま残ります。また、共通のレイアウトコンポーネントだけでなく、ページ内部の一部コンポーネントもアンマウントされず、再レンダリングのみが行われることがあります。これにより、状態管理が期待通りに動作しないケースが発生することがあります。

具体的な例として、useState の初期値はコンポーネントの初回マウント時にのみ使用され、再レンダリング時にはReactが既存の状態値を保持するため、初期値は無視されます。 また、useEffect の依存配列が空配列 [] の場合、そのエフェクトはコンポーネントの初回マウント時にしか実行されません。

ページ遷移の方法が変わったことで、レンダリング時の挙動が変わり、元々の挙動では無くなってしまったのが原因でした。

具体的なコード

このコードは、Next.jsを使用して「/ticket/[id]」のような動的ルーティングを行い、チケット情報を表示するサンプルです。 Chakra UIのLinkコンポーネントとNext.jsのLinkコンポーネントを使い分けて、フルページリロードとSPAスタイルのページ遷移を比較しています。

// pages/ticket/[id].ts
import NextLink from "next/link";
import { useState } from "react";
import { Link } from "@chakra-ui/react";

type Ticket = { id: number; name: string };

const ticketData: Record<number, Ticket> = {
  1: { id: 1, name: "演劇" },
  2: { id: 2, name: "ミュージカル" },
  3: { id: 3, name: "コンサート" },
};

const TicketComponent = ({ id }: { id: string }) => {
  const [ticket, setTicket] = useState(ticketData[parseInt(id)]);
  const ids = ["1", "2", "3"];

  return (
    <>
      <div>
        <h1>チケット情報</h1>
        <p>ID: {ticket.id}</p>
        <p>イベント名: {ticket.name}</p>
      </div>

      <br />

      <div>
        <h2>フルページリロード遷移</h2>
        <ul>
          {ids.map((id) => (
            <li key={id}>
              <Link href={`/ticket/${id}`}>チケット {id} のページへ</Link>
            </li>
          ))}
        </ul>
      </div>

      <br />

      <div>
        <h2>SPA遷移</h2>
        <ul>
          {ids.map((id) => (
            <li key={id}>
              <Link as={NextLink} href={`/ticket/${id}`}>
                チケット {id} のページへ
              </Link>
            </li>
          ))}
        </ul>
      </div>
    </>
  );
};

export default function TicketPage({ id }: { id: string }) {
  return <TicketComponent id={id} />;
}

TicketPage.getInitialProps = ({ query }: { query: { id: string } }) => {
  return { id: query.id };
};

実際にクリックして遷移するとわかるのですが、フルページリロード遷移の場合、ページ遷移するたびにチケット情報が書き変わります。 一方で、SPA遷移した場合、ページ遷移してもチケット情報が書き変わりません。

対策

状態のリセットを行う方法として、key、useEffect と useMemoを使う方法があります。それぞれのメリット・デメリットを理解し、適切に使い分けることが重要です。

コンポーネントにkeyを付与する

Reactにおけるkeyの設定は、コンポーネントの再レンダリングや再マウントの挙動に大きな影響を与えます。keyは、Reactがレンダリングする際に、各要素を一意に識別するために使用されます。keyを設定すると、Reactは要素が変更されたか、追加されたか、削除されたかを効率的に判断できます。

コンポーネントのkeyが変更されると、Reactはそのコンポーネントを「新しいコンポーネント」として扱います。つまり、既存のコンポーネントをアンマウントし、新しいコンポーネントをマウントすることになります。これによって、そのコンポーネントが初期状態から再レンダリングされます。

TicketComponentに、下記のようにkeyを与えてページ遷移すると、SPA遷移であってもチケット情報が書き変わることがわかります。

export default function TicketPage({ id }: { id: string }) {
  return <TicketComponent id={id} key={id} />;
}

デメリットとしては、コンポーネントが再マウントされると、その内部のすべての子コンポーネントも再マウントされます。これにより、不要な再レンダリングやパフォーマンスの低下を招く可能性があります。また、コンポーネントのすべての状態がリセットされるため、リセットが必要ない部分の状態もクリアされてしまう可能性があります。

useEffectを使う

useEffectを使うと、コンポーネントの再マウントを避けつつ、特定の状態やロジックのみを更新できます。これにより、細かな制御が可能です。

TicketComponent内部に、useEffectを追加してページ遷移すると、SPA遷移であってもチケット情報が書き変わることがわかります。

  useEffect(() => {
    setTicket(ticketData[parseInt(id)]);
  }, [id]);

デメリットとしては、状態が複雑な場合や依存関係が多い場合、useEffect での更新が意図しないタイミングで発生することがあり、結果として状態が不整合になる可能性があります。 また、レンダリングが2回行われてしまう問題があります。

useMemoを使う

適切かどうかは別として、useMemoを使うと、期待するページ遷移を行うことができます。

const [ticket, setTicket] = useState(ticketData[parseInt(id)]);

ここを下記のように書き換えてページ遷移すると、SPA遷移であってもチケット情報が書き変わることがわかります。

  const ticket = useMemo(() => ticketData[parseInt(id)], [id]);

大きな問題としては、useEffect や useState を使うべきところで useMemo を誤用すると、期待通りの再レンダリングが発生せず、バグの原因になりやすいです。これは、それぞれのフックが異なる発火タイミングと目的を持っているためです。

これらの違いを理解して適切に使い分けることが重要ですが、長期的なプロジェクト運用を考えると、useMemo の積極的な利用は保守性の観点から避けるべきだと感じています。

おわりに

クライアントサイドナビゲーションを行うと、一部のコンポーネントはアンマウントされず、状態が保持されたままになります。これが、期待通りに動作しない原因となることがあります。 その対策として、key、useEffect、useMemoを使う方法を整理しました。長期的なプロジェクトの保守性を考えると、useMemo の誤用を避け、keyによる再マウント、useEffect や useState の適切な使用が必要だと思いました。

エンジニア募集中!

私たちのチームでは、共にプロダクトを成長させるエンジニアを募集しています。最先端の技術を駆使し、ユーザー体験を向上させることに情熱を持っている方、ぜひ一緒に働きましょう!

詳細は下記リンクよりご覧いただけます。カジュアルな面談も随時受け付けていますので、お気軽にお問い合わせください。

ticketme.notion.site

テックブログを始めます!

はじめに

こんにちは!株式会社チケミーCTOの坂元(0xyusaka)です。
チケミーのテックブログにお越しいただきありがとうございます。

株式会社チケミーは、NFT技術を活用した新しいチケットのカタチを提案しているスタートアップです。
音楽ライブやスポーツイベント、演劇などのチケットを、NFTチケットとして発行・管理するプラットフォームを提供しています。
「NFTチケットって何?」と思う方もいるかもしれませんが、簡単に言うと、ブロックチェーン技術を使って電子チケットを発行する仕組みです。

チケミーでは「チケット」を一般的に想像されるものよりも広い意味で捉えており、モノの引換券や会員券などもNFTチケットとして価値を流通できるようにしています。

また、チケミーでは『あるべき場所に、あるべき価値を届ける』というミッションのもと、健全な二次流通市場を作るためのプロダクト開発に日々取り組んでいます。
技術の力でより良い体験を提供し、業界全体に新しい価値を生み出していきたいと考えています。

このテックブログでは、そんな私たちチケミーの開発の裏側や、日々の挑戦、学びなどをシェアしていきます。
エンジニアの皆さんにとって面白いと感じてもらえる内容をどんどん発信していきたいと思っています!

なぜテックブログを始めたのか?

私たちは、NFTチケットという新しい分野で日々開発を進めています。
ブロックチェーン技術やスマートコントラクト、Webフロントエンド・バックエンドの開発、インフラ構築など、多岐にわたる技術スタックを使用しています。
また、スタートアップにおける効率的な開発手法、チームでの取り組み方など、さまざまなチャレンジもしています。

このブログを通じて、そういったチャレンジの過程やそこで得られた知見をアウトプットし、より良いものにブラッシュアップしていきたいと考えています。

今後のブログ内容について

これからのブログ記事では、具体的な技術的な深掘りから、チケミーのエンジニアチームの日常まで、幅広いトピックをゆるく発信していく予定です。 例えば、以下のような内容を取り上げる予定です。

チケミーではエンジニアを募集中です!

チケミーでは、一緒に未来のチケット体験を創り上げる仲間を募集しています!
エンターテインメント業界を技術の力で変革したい、ブロックチェーン技術やNFTに興味がある、スタートアップならではのスピード感とチャレンジ精神に共感できる。そんな方にぴったりの環境です。

もし少しでも興味を持っていただけたら、ぜひお気軽にご連絡ください。まずはカジュアル面談でお話ししましょう!
X(Twitter)、YOUTRUST、または弊社の採用ページからご連絡ください。皆さんからのご応募をお待ちしています!

https://x.com/0xyusaka

youtrust.jp

ticketme.notion.site