Development Guidelines
フロントエンド実装
フロントエンド実装時のファイル構成・コンポーネント設計・データ取得パターンのガイドライン。
ファイル内の定義順序
抽象から具体の順で定義し、ファイルの構造を統一する。
- Directives:
"use client"/"use server" - Imports: ライブラリや別ファイルの読み込み
- Constants: ファイル全体で使う定数
- Types: 型定義
- Main Component: このファイルの主役(exportするもの)
- Sub Components: このファイルだけで使う小さなコンポーネント
- 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')} />エラー・ローディング表示
useQueryのisLoading/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) は原則使わない