フロントエンド コーディング規約
このドキュメントは、当プロジェクトのフロントエンド開発におけるコードの品質と一貫性を保つための規約をまとめたものです。
A. アプリケーションの構成とナビゲーション定義
プロジェクト全体の構造、ルーティング、ナビゲーション、およびサイトの基本設定に関する規約です。
1. ルーティングと Navigation API
-
ページ遷移に使用する
Linkコンポーネントは、多言語対応のためnext-intl由来のものを利用してください。通常、src/navigation.ts(またはsrc/i18n.tsなどで設定されたエイリアス経由) からインポートします。// 例: src/navigation.ts で next-intl/navigation からエクスポートされたものを使う import { Link } from "@/navigation"; // または、useRouter, redirect, usePathname なども同様 // import { useRouter, redirect, usePathname } from '@/navigation'; -
useRouterやusePathnameなどの Navigation API も同様です。
2. 主要な設定ファイルとフォルダ構成
プロジェクトの保守性と可読性を高めるため、特定の情報を一元管理するファイルを設けます。
-
src/constants/path.ts- 責務: アプリケーション内で使用する全ての主要なルーティングパス(静的パス、動的パスのテンプレート)を定数として一元管理します。
- 目的:
- パスのタイポを防ぐ。
- パス変更時の修正箇所をこのファイルに限定し、影響範囲を最小限に抑える。
- コード内でのパスの可読性を向上させる (
path.ABOUT_USのように利用)。
-
src/constants/navigation.ts- 責務: ヘッダー、フッター、サイドバーなどのナビゲーションメニューで使用するリンクの定義情報を集約します。
- 内容: 各リンクの表示テキスト(多言語対応キー)、遷移先パス(
path.tsから参照)、アイコン、表示条件などをオブジェクトや配列の形で定義します。 - 目的: ナビゲーション構造の変更や追加を容易にし、UI コンポーネントからはこの定義を参照して動的にメニューを生成します。
-
src/constants/siteConfig.ts- 責務: サイト全体に関わる基本的な設定情報やメタデータを定義します。
- 内容: サイト名、サイトのキャッチフレーズや説明文、著作権者情報、デフォルトロケール、などを格納します。
- 目的: サイト全体で共通して利用される情報を一箇所で管理し、変更を容易にします。
3. 国際化 (i18n) と翻訳ファイルの構造
多言語対応における翻訳データ (JSON ファイル) の管理方法と命名規則に関する規約です。
-
基本方針:
-
URL ベースの構造: 翻訳ファイルおよびその内部のキー構造は、原則としてフロントエンドの URL 構造に基づいて整理します。これにより、翻訳対象のコンテンツがどのページやコンポーネントに対応するのかを直感的に把握しやすくします。 また、コンフリクトが起きるリスクを軽減させます。(同一ファイルを複数人が修正するため。) ただし、すべてのページで使われるため、Header と Footer は例外とします。
-
命名規則: ファイル名やキー名は、可読性と一貫性を維持するため、FastAPI の API エンドポイント設計で用いられる命名等を参考に、可能な範囲で統一感を持たせます。
-
-
目的:
- 翻訳作業の効率化と、翻訳データのメンテナンス性の向上を目指します。
- 翻訳漏れや、コンテキストから乖離した翻訳が行われるリスクを低減します。
B. コンポーネント設計と実装ガイドライン
コンポーネントの設計思想、配置ルール、および具体的な実装方法に関する規約です。
1. 主要なコンポーネントディレクトリとその役割
当プロジェクトでは、コンポーネントの保守性、再利用性、および開発効率を高めるため、主に以下の 2 つのトップレベルディレクトリに UI コンポーネントを配置し、それぞれの役割を明確に定めます。
-
src/components/[atoms, molecules, organisms]:- 責務: プロジェクト全体で広く再利用される、汎用的な UI コンポーネント, UI プリミティブライブラリを格納します。これらのコンポーネントは、特定のドメイン知識(アプリケーション固有のビジネスロジックや文脈)を持たず、UI の基本要素として機能します。
- 内容: Atomic Design の考え方に基づき、主に
atoms(最小単位の UI 要素)、molecules(atoms を組み合わせた汎用的な小さな UI 部品)、そして必要であればドメイン知識を持たない汎用的なorganisms(より大きな UI 構造体)を配置します。 - 再利用の目安: 複数の異なる「機能体験グループ」(後述の
src/components/features/を参照)や、アプリケーションの広範囲な箇所で共通して使用されることを想定します。
-
src/components/features:- 責務: アプリケーション内の主要な機能体験グループごとに、UI コンポーネントおよび関連モジュールを整理・格納します。「機能体験グループ」とは、ユーザーが一連の流れとして体験する複数の関連画面や機能のまとまりを指します(例: クイズの受験から結果表示までの一連の体験、レッスンの一覧表示から詳細閲覧までの一連の体験など)。
- 内容:
- 特定の機能体験グループに特有のコンポーネントを配置します。これには、そのグループ内の複数の画面や箇所で共通して使用される UI 部品や、そのグループを構成する主要な UI コンポーネントが含まれます。
- これらのコンポーネントは、
src/components[atoms, molecules, organisms]の汎用 UI 部品を組み合わせて構築されることが多く、該当する機能体験グループのドメイン知識やビジネスロジックを含みます。 - Atomic Design の考え方を応用し、その機能体験グループ固有の
molecules、organisms、あるいはtemplates(特定の画面レイアウトパターン)に相当するコンポーネントがここに格納されます。ただし、フォルダには分けなくて結構です。
- グルーピングの例:
src/components/features/SectionQuiz(クイズ体験グループ): クイズ出題画面、解答画面、結果表示画面などで使われるコンポーネント群。
2. Atomic Design の適用と考え方
当プロジェクトでは、UI コンポーネントの設計と整理の指針として Atomic Design の考え方を採用します。コンポーネントは、その役割と配置場所に応じて、atoms、molecules、organismsといった粒度に分類されます。
-
Atomic Design の基本原則:
Atoms(原子): UI を構成する最小単位の要素(例: ボタン、ラベル、入力フィールド、アイコン)。主にsrc/components/atoms/に配置されます。Molecules(分子): 複数のAtomsを組み合わせて作られる、機能を持つ最小限の UI 部品。Organisms(有機体): 複数のMoleculesやAtomsを組み合わせて構成される、より具体的で独立した UI セクション。
-
MoleculesとOrganismsの違いと配置場所による責務:-
Molecules(分子):src/components/molecules/に配置される場合:- 複数の汎用
Atomsで構成され、特定のドメイン知識を極力持たない、汎用的な UI パーツです。(例: アイコン付きの汎用ボタン、ラベルと入力フィールドがセットになった汎用フォーム要素) - アプリケーション内の様々な「機能体験グループ」等で再利用されることを目指します。
- 複数の汎用
-
Organisms(有機体):src/components/organisms/に配置される場合 (もしあれば):- 複数の汎用
MoleculesやAtomsで構成される、ドメイン知識を持たないが自己完結した汎用的な UI セクションです。(例: カスタマイズ可能な汎用データテーブルコンポーネント、汎用的なカードレイアウトコンテナ)
- 複数の汎用
-
-
その他:
- 同種の中に同種のものを含めることは可能です。例えば、
Moleculesの中にMoleculesのコンポーネントを含めるなど。 - MUI のコンポーネントを直接ラップする場合に限り、接頭辞として
Customをつけます。これは、MUI の単一の既存コンポーネントをベースに、スタイルや props、振る舞いを拡張・変更するコンポーネントを指します。(これらの多くはsrc/components/atoms/やsrc/components/molecules/に配置されます。)
- 同種の中に同種のものを含めることは可能です。例えば、
3. イベントハンドラの命名規則
-
handleプレフィックスの使用禁止: イベントハンドラ関数や Props 名において、handleSubmitやhandleClickのようなhandleで始まる命名は禁止します。 -
アクションの明示:
onClick、onSelect、onChangeのように、具体的な DOM イベントやコンポーネントのアクションを直接示す命名規則 (on+Action) を採用します。- 目的: コンポーネントの Props を見るだけで、その関数がどのようなユーザーインタラクションによってトリガーされるのかを直感的に理解できるようにするためです。
C. スタイリングとテーマ利用規約
UI の見た目に関するスタイリング方法、Typography の定義と利用、テーマファイルへの準拠に関する規約です。
1. Typography と theme.ts への準拠
Figma よりテキストのスタイルがグループ化されいているため、それらをtheme.tsにまとめました。
- テキストのスタイル(フォントサイズ、ウェイトなど)は、原則として
src/theme/theme.ts(または相当するテーマファイル) に定義されたタイポグラフィ定義に従って適用してください。 variantで該当するものを指定します。componentsで HTML の要素を指定します。
2. スタイリング手法 (sx プロパティと styled コンポーネント)
MUI (@mui/material) などのライブラリを使用する場合のスタイリング方針です。
-
styled(例:@mui/material/stylesのstyled):-
用途: 複数箇所で再利用するスタイルを持つコンポーネントや、複雑なスタイルロジック(props に応じた動的なスタイル変更など)を持つコンポーネントを作成する場合に使用します。
-
インポート:
styledユーティリティは、原則として@mui/material/stylesからインポートします。- 理由:
@mui/material由来のstyledを利用することで、コンポーネント内でthemeオブジェクトへ容易にアクセスでき、テーマに沿った動的なスタイリング(例: ダークモード対応、テーマカラーの利用)を効率的に実装できます。
- 理由:
-
コンポーネントとしての意味論を明確にし、カプセル化されたスタイルを提供します。
-
例:
import { styled } from "@mui/material/styles"; import Button from "@mui/material/Button"; const StyledButton = styled(Button)(({ theme }) => ({ backgroundColor: theme.palette.primary.main, /* ...other styles */ }));
-
-
sxプロパティ:- 用途: 特定の要素に対して一度きりの、軽微なスタイル調整やテーマ値を利用した細かいオーバライドを行いたい場合に使用します。
- コンポーネント定義を新たに行うほどではない、局所的なスタイル適用に適しています。
- 例:
<Box sx={{ mt: 2, backgroundColor: 'primary.main' }}>...</Box> - 注意:
sxプロパティを多用しすぎると、コンポーネントの可読性が低下したり、スタイルが分散しやすくなるため、再利用性や複雑性を考慮してstyledと使い分けてください。
3. レイアウトの実装方針
Gridコンポーネントの積極利用: コンポーネント内の要素の配置や、ページ全体のレイアウトを組む際には、原則として@mui/material/Gridコンポーネントを積極的に使用してください。- CSS の Flexbox (
display: 'flex') をsxプロパティなどで直接記述するのではなく、Gridのcontainer、spacing、directionなどの Props を活用します。 - 目的: プロジェクト全体で一貫性のあるグリッドシステムに基づいたレイアウトを効率的に構築し、コードの可読性を高めます。また、ブレークポイント指定によるレスポンシブデザインへの対応も容易になります。 承知いたしました。ご指摘の点を補足し、より明確なガイドラインになるように更新します。
- CSS の Flexbox (
意図的に特定の汎用エラーメッセージを表示したい場合の規約を追記しました。
D. フォームの実装 (Zod + React Hook Form + MUI)
アプリケーション内のフォームは、型安全なバリデーションと効率的な状態管理を実現するため、以下のライブラリを組み合わせて実装することを原則とします。
playground/page.tsxとそれに対応するコンポーネントを参照して作成して下さい
- バリデーション: Zod
- フォーム状態管理: React Hook Form
- UIコンポーネント: MUI
1. バリデーションスキーマの定義 (Zod)
-
フォームの各フィールドのデータ型、必須チェック、フォーマット(メールアドレスなど)、文字数制限などのバリデーションルールを Zod のスキーマとして定義します。
-
z.inferを用いて Zod スキーマから TypeScript の型を自動生成し、フォームデータの型として利用します。これにより、バリデーションルールとコード上の型定義の乖離を防ぎます。 -
エラーメッセージもスキーマ定義内に記述します。
(例:
page.tsxのloginSchema)
2. フォームコンポーネントの実装 (useForm)
-
フォームを実装するコンポーネント内で
useFormフックを呼び出します。 -
resolver:zodResolverを設定し、手順 1 で定義した Zod スキーマを渡してバリデーションを連携させます。 -
mode: ここは臨機応変に選んでください。例えば、'all'を設定すると、入力中およびフォーカスアウト時にバリデーションが実行されます。 -
defaultValues: フォームの初期値を設定します。(例:
playground/page.tsxのLoginPageコンポーネント)
3. RHF 対応 UI コンポーネントの責務と実装
MUI のコンポーネントを React Hook Form (RHF) で管理するため、useController フックを用いたラッパーコンポーネントを作成します。
-
コンポーネントの責務 (
RhfTextareaなど)- RHF の
controlオブジェクトとフィールドのnameを props として受け取ります。 useControllerを用いて RHF の状態 (field,fieldState,formState) を取得します。- 取得した
fieldプロパティ (onChange,onBlur,value等) を、MUI コンポーネントの対応する props に渡します。 formState.isSubmittingを利用して、フォーム送信中はコンポーネントを無効化します。fieldState.errorを検知し、エラー状態のスタイルを適用します。
- RHF の
-
共通ラッパーの責務 (
FormItemWrapper)- フォームアイテムの
labelとerrorオブジェクトを props として受け取ります。 - ラベルと、エラーが存在する場合のエラーメッセージ(
error.message)の表示を担当します。 - これにより、フォームアイテムごとのレイアウトとスタイルの一貫性を保ちます。
- 基本的にこのコンポーネントでラップして新しいRhfコンポーネントを作成して下さい。
(例:
RhfTextField.tsx,FormItemWrapper.tsx) - フォームアイテムの
4. フォームの構築と送信処理
-
useFormから返されたcontrolを、手順 3 で作成した各 RHF 対応コンポーネントに渡します。 -
form要素のonSubmitイベントに、methods.handleSubmitを渡して送信処理を実装します。 -
handleSubmitは、バリデーションが成功した場合にのみ、引数で渡されたコールバック関数(フォームデータが引数)を実行します。(例:
playground/page.tsxの JSX 部分)
E. エラーハンドリングとメッセージ表示
アプリケーション全体で一貫性のあるエラーメッセージを提供し、多言語対応を容易にするための規約です。
1. エラーメッセージの一元管理 (src/constants/errorMessage.ts)
-
責務:
- アプリケーションで扱う可能性のあるエラー(Firebase 認証エラー、汎用的な API エラーなど)と、それに対応する多言語化キー(i18n キー)をマッピングし、一元管理します。
- これにより、エラーコードから表示すべきメッセージへの変換ロジックを統一し、メンテナンス性を向上させます。
-
ファイル構成:
FIREBASE_ERROR_MESSAGES: Firebase 関連のエラーコードと i18n キーのマッピング。GENERAL_ERROR_MESSAGES: ネットワークエラーや CRUD 操作の失敗など、汎用的なエラーケースと i18n キーのマッピング。
-
メンテナンス方法(新しいエラーを追加する場合):
errorMessage.tsを更新:FIREBASE_ERROR_MESSAGESまたはGENERAL_ERROR_MESSAGESオブジェクトに、新しいエラーコードと対応する i18n キーのペアを追加します。// 例: GENERAL_ERROR_MESSAGES にバリデーションエラーを追加する場合 export const GENERAL_ERROR_MESSAGES = { // ...既存のエラー VALIDATION_FAILED: "Common.Errors.general.validation_failed", // 追記 } as const;
- 翻訳ファイル (
*.json) を更新:- すべての言語の翻訳ファイル(例:
messages/en.json,messages/ja.json)に、手順 1 で追加した i18n キーと、それに対応するエラーメッセージの翻訳文を追加します。// 例: messages/ja.json { "Common": { "Errors": { "general": { "validation_failed": "入力内容に誤りがあります。" } } } }
- すべての言語の翻訳ファイル(例:
2. エラーメッセージの翻訳と利用方法
エラーオブジェクトやエラーコードを、ユーザーに表示するための翻訳済みメッセージに変換するユーティリティを用意しています。コンポーネントの種類(クライアント or サーバー)に応じて適切なものを利用してください。
a. クライアントコンポーネントでの利用 (useTranslatedError フック)
"use client" ディレクティブを持つクライアントコンポーネント内でエラーメッセージを取得する場合は、useTranslatedError フックを使用します。
-
提供元:
useTranslateError.ts -
関数:
getErrorMessage(error: unknown): string -
使い方:
- コンポーネントのトップレベルで
useTranslatedErrorを呼び出してgetErrorMessage関数を取得します。 try...catch構文などでキャッチしたerrorオブジェクトをgetErrorMessageに渡すことで、翻訳された文字列を取得できます。
- コンポーネントのトップレベルで
-
コード例:
"use client"; import { useState } from "react"; import { useTranslatedError } from "@/hooks/useTranslatedError"; // パスはプロジェクト構成に合わせてください import { someAsyncFunction } from "@/lib/actions"; // エラーをスローする可能性のある非同期関数 export const MyComponent = () => { const { getErrorMessage } = useTranslatedError(); const [errorText, setErrorText] = useState<string | null>(null); const handleClick = async () => { setErrorText(null); try { await someAsyncFunction(); } catch (error) { // キャッチしたエラーを渡して翻訳済みメッセージを取得 const translatedMessage = getErrorMessage(error); setErrorText(translatedMessage); } }; return ( <div> <button onClick={handleClick}>実行</button> {errorText && <p style={{ color: "red" }}>{errorText}</p>} </div> ); };
b. サーバーコンポーネント / Server Actions での利用 (getServerErrorMessage 関数)
サーバーコンポーネントや Server Actions など、サーバーサイドの非同期処理中でエラーメッセージを生成する必要がある場合は、getServerErrorMessage 関数を使用します。
-
提供元:
serverTranslatedError.ts -
関数:
async getServerErrorMessage(error: unknown): Promise<string> -
使い方:
- サーバーサイドの関数内で
getServerErrorMessageをawaitをつけて呼び出します。 - 引数にキャッチした
errorオブジェクトを渡します。
- サーバーサイドの関数内で
-
コード例 (Server Action):
"use server" //例をわかりやすくするために明示的にしています。 import { getServerErrorMessage } from "@/lib/serverTranslatedError" // パスはプロジェクト構成に合わせて export async function loginAction(formData: FormData) { try { ...処理 } catch (error) { // サーバーサイドでエラーを翻訳 const errorMessage = await getServerErrorMessage(error) // 翻訳済みメッセージをクライアントに返す return { error: errorMessage } } }
c. 意図した汎用エラーメッセージの表示
Firebase などからスローされるエラーだけでなく、独自のバリデーションチェックなど、特定の条件で意図的に定義済みエラーメッセージを表示したい場合があります。その場合は、src/constants/errorMessage.ts の GENERAL_ERROR_MESSAGES に定義されたエラーキー(文字列)を利用します。
-
方法:
- ビジネスロジック内でエラー条件を判定します。
- 条件に合致した場合、
GENERAL_ERROR_MESSAGESに定義されている適切なエラーキー(例:'UPDATE_FAILED')をthrowします。 catchブロックでそのエラーキーを捕捉し、getErrorMessageまたはgetServerErrorMessageに渡すことで、対応する翻訳メッセージを取得できます。
-
コード例 (Server Action でのバリデーション):
アクション関数内で入力値が不正である場合に、定義済みのエラーキーを
throwします。try...catchでそれを捕捉し、クライアントに返すオブジェクトに格納することで、一貫したエラーハンドリングを実現します。"use server"; import { getServerErrorMessage } from "@/lib/serverTranslatedError"; // このアクションはフォームから呼び出されることを想定 export async function updateUserAction(formData: FormData) { try { const username = formData.get("username"); // 独自のバリデーション if (typeof username !== "string" || username.length < 3) { // 定義済みの汎用エラーキーをスローする // ここでは 'UPDATE_FAILED' を例として使用(より適切なキーがあればそれを使用) throw "UPDATE_FAILED"; } // ...ユーザー更新処理... return { success: true, error: null }; } catch (error) { // 自身で投げたエラーキーも、DB等からスローされたエラーも、 // getServerErrorMessage が適切に処理してくれる const errorMessage = await getServerErrorMessage(error); return { success: false, error: errorMessage }; } } -
ポイント:
throw new Error("ユーザー名が短すぎます")のようにエラーメッセージを直接ハードコードするのではなく、errorMessage.tsに定義されたキー('UPDATE_FAILED'など)をthrowします。- これにより、エラーメッセージの文言変更や多言語対応が、翻訳ファイルと
errorMessage.tsの修正だけで完結するようになります。
F. BFFごしでのFastAPIのコール方法
セキュリティの都合も兼ねてNextソース上のAPIをプロキシにFastAPIを呼び出ししています。 \
ざっくりと構成は下記になります。
BFF本体のサンプル
front/src/app/api/proxy/users/route.ts
tanstackを利用したmutationの記載
front/src/hooks/users.ts
上記mutationを利用してAPIをコールしている個所 2025/07/05現在ダミーで記載中
front/src/app/[locale]/(main)/dashboard/page.tsx
useMutationを利用することでレスポンスを保存できるため不要なAPIコールの削減に寄与したり、ローディング等の状態管理などにも役立ちます。
回りくどいですが意識して実装を行うようにお願いいたします。
useQuery内のqueryKeyですがパスをラワーキャメルで取ってくださいusers/detail -> usersDetailのように。