BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

システムリニューアルでNext.jsのApp Router/Server Actionを使って便利だと思ったところ

はじめに

こんにちは、Pay IDのフロントエンドエンジニアのnojiです。

普段はPay ID あと払いやPay IDのアカウント周りのフロントエンド開発を担当しています。

10月にPay IDのアカウント編集画面こちら)をリニューアルしました。この記事では、そのリニューアルプロジェクトでNext.jsのApp Router / Server Actionを活用し、便利だと感じた点をご紹介します。

使用技術

  • Next.js 14(リリース当時のもの。現在は15になっています)
  • React 18(リリース当時のもの。現在は19になっています)

リニューアルの背景

今回のリニューアルは、PAY社が保有する「Pay ID」のデータおよびシステムをBASE社へ移管・再構築するプロジェクトの一環でした。

単なる移管に留まらず、デザインリニューアルを伴うため、PAY社で使用されていたVue.jsのコードは再利用せず、Next.jsでゼロから構築しました。

Next.jsを選定した理由:

  1. BASE社内での採用実績があり、安心して使用できること。

  2. BASE以外でも採用事例が多かったり、近年のfrontendのトレンドをリードしていること

また、新規アプリケーションの構築であることから、Next.jsのApp Routerを採用しました。(開発着手当初はNext.jsバージョン13でした)

App Routerの活用

App Routerはディレクトリ構造がそのままURLパスとして反映されるほか、ファイル名によって役割が明確になる点が特徴です。

/app/〇〇
  └ layout.tsx // pageをラップした共通画面(画面遷移で再レンダリングされない)
  └ template.tsx // pageをラップした共通画面(画面遷移で再レンダリングされる)
  └ error.tsx // エラー時の画面
  └ loading.tsx // ローディング中の画面
  └ not-found.tsx // notFound()がthrowされたときの画面
  └ page.tsx // 実際のURLに対応するページ

これをもとに以下のようなツリー構造で描画が行われます

<Layout>
  <Template>
    <ErrorBoundary fallback={<Error />}>
      <Suspense fallback={<Loading />}>
        <ErrorBoundary fallback={<NotFound />}>
          <Page />
        </ErrorBoundary>
      </Suspense>
    </ErrorBoundary>
  </Template>
</Layout>

親ディレクトリのLayoutを子ページが自動的に引き継ぐため、レイアウト部分を共通化しやすいのが便利でした。

https://nextjs.org/docs/app/building-your-application/routing

Route Groups

一方で、「URL階層は親子関係があるがレイアウトを切り替えたい」というケースでは、Route Groupsを活用しました。

Route Groupsはディレクトリ名を ()で括ることで利用できます。

(a)
  └ hoge
    └ layout.tsx ← レイアウトA
      └ page.tsx
(b)
  └ hoge
    └ layout.tsx ← レイアウトB
      └ fuga
        └ page.tsx
  • /hogeではレイアウトAを利用
  • /hoge/fugaではレイアウトBを利用

このように適切にレイアウトを切り替えられるのが便利でした。

https://nextjs.org/docs/app/building-your-application/routing/route-groups

Parallel Route

管理画面では、ダッシュボードのように複数の情報を1ページに集約して表示することが多く、特定のページでParallel Routesを利用しました。

app
  └ @parallel
    └ hoge
      └ page.tsx  // A情報
      └ default.tsx
    └ default.tsx
  └ hoge
    └ page.tsx // B情報

@で始まるディレクトリがslotとなり、同じ階層にあるlayout.tsxでpropsとして受け取ります。

default.tsxは初期読み込み時やページ全体の再読込中に一致しないスロットのフォールバックとしてレンダリングするファイルを定義しています。 何も表示させない場合はnullを返却するように定義しておきます。

// layout.tsx
export default async function Layout({
    children,
    parallel,
}: {
    children: React.ReactNode;
    parallel: React.ReactNode;
}) {
  return (
    <>
        {parallel} // A情報
      {children} // B情報
    </>
  );
}

この仕組みを利用し、A情報とB情報をそれぞれ別のページとして描画し、以下のように表示しました:

  • /app/@parallel/hoge/page.tsx → A情報を表示
  • /app/hoge/page.tsx → B情報を表示

Parallel Routesの利用により、slotに分割されていることで各ページでの処理が少なくなったり、コードの可読性が上がるように感じました。

Parallel routeでもloading.tsxやerror.tsxを配置しておくだけでローディング処理やErrorBoundaryを利用できるので便利に感じました。片方エラーだったときのハンドリング等しやすい印象を受けました。

しかし、soft navigation時に前のParallelRouteのページが表示されたままの状態になることがあり、Client Componentでラップして、pathを見て出し分けするようにし、意図せず表示が残ってしまうことを防いでいます。

// MatchPathRenderer.tsx
'use client';

export default function MatchPathnameRenderer({ matches, children }: Props) {
  const pathname = usePathname();
  if (!matches || !matches.includes(pathname)) {
    return null;
  }
  return <>{children}</>;
}
// layout.tsx
...

  <MatchPathnameRenderer matches={['/hoge']}>
    {parallel}
  </MatchPathnameRenderer>
  {children}
...

https://nextjs.org/docs/app/building-your-application/routing/parallel-routes

Server Component / Server Actionの活用

Server側でのデータ処理周り

各Server Componentからサーバー側で非同期にデータフェッチが可能となり、データ管理が効率的に行えるようになりました。また、同じデータのフェッチ処理は自動的にキャッシュされるため、パフォーマンスへの影響も最小限に抑えられます。

さらに、App RouterのSuspense処理(loading.tsx)を活用することで、サーバーから先にHTMLを返却し、高いユーザー体験を簡単に実現できるようになりました。

サーバー側で動作するため、機密情報やセキュリティに関わるデータも安全に取り扱うことが可能です。

クライアントからサーバーへのリクエスト周り

従来、クライアントからNext.jsのサーバーへリクエストを送る際には、API Routeを定義し、それを通じてAPIを実行する必要がありましたが、Server Actionを使用することで、クライアント側から直接サーバー側の関数を呼び出せるようになり、非常に便利に感じました。

// server action処理
'use server';

export const action = async (prevState: any, formData: FormData) => {
  // server側処理
}
// フォームコンポーネント
'use client';

export default function Form() {
  const [state, formAction] = useFormState(action);

  return (
    <form action={formAction}>
      ...
    </form>
  )
}

https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations

リダイレクト周り

アカウント管理画面では、認証が必須のため、各URLで適切なリダイレクト処理が必要だったのですが、Server ComponentServer Action内でredirectを利用することができます。

Server Component内でのリダイレクトはPage RouterでのgetServerSidePropsと似たような処理が実現できます。Server Component内にリダイレクト処理が書けるようになった部分は少し可読性が上がったように感じました。

// Server Component内
export default async function Page() {
  const session = await getSession(); // セッション取得処理
  if (!session) {
    return redirect('/login');
  }
  const { response, error } = getData(session); // データ取得処理
  if (error.status === 401) {
    return redirect('/login');
  }
  ...
}

Server Action内のリダイレクト処理については以前だとAPI Routeでクライアントにレスポンスを返してからその結果を見てページ遷移を行っていましたが、サーバー側で処理を完結させることができるため、リダイレクト先のページ表示が高速化されました。

// Server Action内
'use server';

export const action = (formData: FormData) => {
  const session = await getSession(); // セッション取得処理
  if (!session) {
    return redirect('/login');
  }
  // API callなどサーバー処理
  return { message: '成功しました' };
};

https://nextjs.org/docs/app/building-your-application/routing/redirecting

キャッシュ周り

Server Actionでサーバーサイドの処理を実行した後、クライアントの情報(キャッシュ含む)を更新する際にrevalidatePathを利用しました。クライアント側でレスポンスを受け取り、値に基づいてUIを更新したり、router.pushrouter.refreshで明示的にページを遷移させる必要がなくなりました。

'use server';

export const action = (formData: FormData) => {
  // API callなどサーバー処理
  revalidatePath('/hoge');
  return { message: '成功しました' };
};

https://nextjs.org/docs/app/building-your-application/caching#revalidatepath

おわりに

システムリニューアル時にNext.jsのapp router / server actionsを触ってみて便利だった部分を紹介させていただきました。

これからNext.jsを利用するプロジェクトの参考になれば幸いです

現在Pay IDではエンジニアを募集しているので、興味のある方は気軽にご応募ください。