Foundation
Development Guidelines

フロントエンド実装

フロントエンド実装時のファイル構成・コンポーネント設計・データ取得パターンのガイドライン。

ファイル内の定義順序

抽象から具体の順で定義し、ファイルの構造を統一する。

  1. Directives: "use client" / "use server"
  2. Imports: ライブラリや別ファイルの読み込み
  3. Constants: ファイル全体で使う定数
  4. Types: 型定義
  5. Main Component: このファイルの主役(exportするもの)
  6. Sub Components: このファイルだけで使う小さなコンポーネント
  7. Helpers / Utils: ロジックだけの純粋関数やhooks

関数の定義スタイル

  • exportする関数はアロー関数で定義する(ESLintで強制される)
  • exportしない関数は function 宣言で定義する(hoistingにより、定義前に参照できる)
// exportする関数: アロー関数
export const EmployeeList = () => {
  const items = formatItems(data);
  return <ul>{items.map((item) => <li key={item.id}>{item.name}</li>)}</ul>;
};

// exportしない関数: function宣言(hoistingで上のコンポーネントから参照できる)
function formatItems(data: Employee[]) {
  return data.map((d) => ({ id: d.id, name: d.displayName }));
}

Server Component / Client Component

CSRメインの設計とする。SEOは重視せず、SSRによるデータ取得は行わない。

page.tsx の役割(Server Component)

page.tsx は薄いServer Componentとし、以下のみを担当する。

  • metadata / generateMetadata によるページタイトルの設定
  • _components/ 内のメインコンポーネントを呼び出す
// app/employees/page.tsx
import type { Metadata } from 'next';

import { EmployeePage } from './_components/employee-page';

export const metadata: Metadata = {
  title: '従業員一覧',
};

// NOTE: default exportはpage/layout等の特殊ファイルでのみ許可される
export default function Page() {
  return <EmployeePage />;
}

_components/ のメインコンポーネント(Client Component)

データ取得・UI・インタラクションはすべて _components/ 内のClient Componentで行う。

layout.tsx(Server Component)

layout.tsx はServer Componentのまま維持する。Providerのラップ、フォント設定、metadata templateを担当する。

tRPC の呼び出しパターン

  • useTRPC() でクライアントを取得し、useQuery(trpc.xxx.queryOptions()) でデータを取得する
  • Mutation後は queryClient.invalidateQueries() で関連キャッシュを無効化する
'use client';

import { useQuery, useQueryClient } from '@tanstack/react-query';

import { useTRPC } from '@/lib/trpc';

export const EmployeePage = () => {
  const trpc = useTRPC();
  const { data, isLoading, isError } = useQuery(trpc.employee.list.queryOptions());

  if (isLoading) return <Loading />;
  if (isError) return <Error />;

  return <EmployeeList employees={data} />;
};

UI コンポーネントの選定

  • @bi-shop-it/ui デザインシステムのコンポーネントを使う
  • DSに該当コンポーネントがない場合は、UIライブラリのリポジトリ(bi-shop-it/foundation)へのIssue起票をユーザーに提案する。自動で起票してはならない
  • Tailwind CSSのユーティリティクラスを直接書くことはなるべく避け、DSのコンポーネントとデザイントークンを使う

スタイリング

条件付きクラスの結合には cn() (@/lib/utils) を使う。

import { cn } from '@/lib/utils';

<div className={cn('base-class', isActive && 'active-class')} />

エラー・ローディング表示

  • useQueryisLoading / isError で条件分岐する
  • 共通コンポーネント <Loading />, <Error /> (@/components/feedback/) を使う

Mutation エラーメッセージ

  • バックエンドのエラーメッセージは開発者向けであり、ユーザーにそのまま表示しない
  • ユーザー向けメッセージはフロントエンドで決定する。getErrorMessage() (@/lib/trpc-error) にエラーコードとメッセージの対応表を渡す
  • 対応表の値は静的な文字列、またはコンテキストを受け取る関数で定義できる
const errorMessage = getErrorMessage(error, {
  CONFLICT: 'このメールアドレスは既に使用されています',
  NOT_FOUND: (ctx) => `${ctx.resource}が見つかりませんでした`,
});

コンポーネントの置き場所

  • コンポーネントは最初、必要なページの _components/ に置く
  • 複数のページで必要になった時点で src/components/ に移動する

コンポーネントの実装

Props の定義

  • Props の型名は ComponentNameProps とする
  • exportするコンポーネントのPropsはインラインではなく、事前にtypeで定義する
  • exportしないプライベートコンポーネントのPropsはインラインで定義してよい
// exportするコンポーネント: Propsを事前に定義する
type EmployeeCardProps = {
  name: string;
  role: string;
};

export const EmployeeCard = ({ name, role }: EmployeeCardProps) => {
  return (
    <Card>
      <EmployeeAvatar name={name} />
      <span>{role}</span>
    </Card>
  );
};

// プライベートコンポーネント: インラインで定義してよい
function EmployeeAvatar({ name }: { name: string }) {
  return <Avatar>{name[0]}</Avatar>;
}

配置の責務

配置に関する知識(余白・位置)は親コンポーネントが持つべきである。

  • コンポーネントの一番外側の要素に margin を付けない
  • <></> (Fragment) は原則使わない

On this page