mcpField
F
frontend-architecture

frontend-architecture

github.com/we-are-singular開発254 回インストール3 週間前に更新
リポジトリ

frontend-architecture について

ページ、コンポーネント、共有 UI、データ層を扱う TypeScript フロントエンド開発者向けの、フレームワーク対応アーキテクチャスキルです。プロジェクトの規約と責務の明確化を重視し、アプリ固有の配線と再利用可能な UI を分離します。

主な機能

  1. 01リポジトリの規約を先に確認
  2. 02フレームワークと境界の整理
  3. 03コントローラーと UI プリミティブの分離
  4. 04型付きサーバー状態クエリの構成
  5. 05アクセシブルな操作パターンの適用

活用シーン

  1. 01不慣れなリポジトリでの変更計画
  2. 02責務が混在するコンポーネントのリファクタリング
  3. 03アプリ依存のない共有 UI の設計
  4. 04型付き API 取得とキャッシュ更新の整理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/we-are-singular/skills --skill frontend-architecture

インストール後、Claude Code で/frontend-architectureを入力して読み込まれたか確認します。

詳細

フロントエンドアーキテクチャ

このスキルは、TypeScript を使ったフロントエンド開発で使用します。フレームワークを考慮しつつ、特定のプロジェクトには依存しません。同じ役割を各プロジェクトのフレームワークに合わせて対応付けることで、React アプリ、Astro のアイランド、Next.js のページ、共有 UI パッケージ、デザインシステムのコンポーネント、型付き API クライアント、フォームが多い画面、ブラウザー向けの統合モジュールに適用できます。

最初にすること

コードを変更する前に、次のことを行います。

  • まず、リポジトリのガイダンスを読みます。AGENTS.md、パッケージの README、コンポーネントのドキュメント、ローカルのテスト設定、周辺のコードを確認します。
  • フレームワークの境界を特定します。静的ページ、サーバーレンダリングされるページ、React アイランド、クライアント専用コンポーネント、ルートハンドラー、共有 UI パッケージ、生成されたクライアントパッケージのどれに該当するかを確認します。
  • 既存のインポートエイリアス、命名規則、テストヘルパー、クエリークライアントの設定、ストアヘルパー、フォームユーティリティ、分析の規約、共有 UI プリミティブを確認します。
  • リポジトリの規約が明確な場合は、それに従います。このスキルは、新しいコードを書く場合、コードの扱いが明確でない場合、またはアーキテクチャの整理が必要な場合のデフォルトとして使用します。
  • 新しいコンポーネント、フック、サービス、ストア、クエリーキー、スキーマ、バリアント、ヘルパーを作成する前に検索します。似たコードを追加するのではなく、類似するコードを統合します。
  • DTO、スキーマ、ルートコントラクト、生成されたクライアント、デザイントークンについて、重複して作成する前に信頼できる唯一の情報源を特定します。

基本方針:UI を使いやすく保ち、データの所有者を明確にし、共有コンポーネントにアプリ固有の動作を持たせないようにします。

基本原則

  • フロントエンドのコードには TypeScript を使用します。厳密な props、明示的なエクスポート戻り値の型、緩いオブジェクトよりも堅牢なドメイン型を優先します。
  • まずローカルのプリミティブを活用します。既存の UI コンポーネント、cn() ヘルパー、バリアントヘルパー、フォームラッパー、クエリヘルパー、テストユーティリティを使用します。
  • コンポーネントは小さく、役割を絞ります。複数の責務を持つ場合、およそ 100 行を超える場合、または同じパターンが 2 か所で使われる場合は切り出します。
  • props のバケツリレーや props の多い API より、コンポジションを優先します。描画可能な領域やアクションには、children、スロット、レンダープロップ、複合コンポーネントを使用します。
  • 使用しないコンポーネントを経由して、props を 1 階層以上バケツリレーすることは避けます。呼び出し元にコンポジションを引き上げるか、役割を絞ったプロバイダー/ストアを導入します。
  • 適切な場合は、すべての値を中間コンポーネント経由で渡すのではなく、確立されたプロバイダーやフレームワークのローカル変数から、コンポーネントがアプリのデータ、パス、ロケール、認証/セッション、その他のアプリコンテキストを読み取るか導出できるようにします。props は直近の境界と、最終的なプレゼンテーション用コンポーネントに使用します。
  • UI やドメイン上の責務に沿った名前を付けます。ResourceTable、UserMenu、createResourceQueryOptions、resourceService、$activePanel のようにします。
  • ファイルが実際にフレームワークの接着役にすぎない場合を除き、Manager、Helper、Util、Wrapper のような曖昧な名前は避けます。
  • 略語より明確な名前を優先します。可能な限り関数は純粋に保ち、変換ロジックを描画や副作用から分離します。
  • 呼び出し元が分岐する必要のある想定内の失敗には、結果型を使用します。ローカルのスタックで通常のエラー/例外フローが期待される場合は、React Query、tRPC、フォームライブラリ、外部連携の境界でそのフローを使用します。
  • コントローラーのコードとプレゼンテーション用プリミティブを分離します。コントローラーはルート、クエリ、分析、機能固有のコールバックを扱えます。プリミティブにはデータとコールバックを渡します。
  • 複雑なインタラクションでは、DOM、状態機械、データ取得を混在させた大規模なコンポーネントより、ヘッドレスフックと小さなプリミティブのツリーを優先します。
  • 生成ファイルであることを明確に示し、リポジトリで明示的に求められている場合を除き、手作業で編集しないようにします。
  • 再利用が実際に必要になるまでは、抽象化をローカルに保ちます。責務の所在を明確にしたり、実際のパッケージ境界を支えたりしない限り、ヘルパーファイル、バレルファイル、単一目的の間接化を追加しないでください。
  • 「スパゲッティ DRY」は避けます。集約がよいのは、呼び出し箇所と責務の所在を単純化できる場合に限ります。
  • 将来の状態を推測して作らないでください。将来のフィールドや動作がまだ実装されていない場合は、推測に基づくロジックではなく、明確な TODO を残します。
  • 個人の好みより、リポジトリ内のフォーマット、リンティング、インポート順序、アクセシビリティ、ファイル命名の規則に従います。

アプリの境界

コードを書く前に、責務を整理します。

ページ、レイアウト、ルート

ページとルートファイルでは、ルーティングに関する処理を担当します。

  • ルートパラメーター、検索パラメーター、リダイレクト、ステータスコード、メタデータ。
  • ページレベルのデータ依存関係の選択。
  • レイアウトと機能コンポーネントの組み合わせ。
  • 子の境界に必要なデータのみを渡すこと。

可能な限り、複雑なインタラクションロジック、キャッシュ更新、フォーム、レンダリング分岐をルートファイルに含めないでください。明確な責務を持つ機能コンポーネント、サービス、フックに移してください。

Astro 形式のアプリでは、ドキュメント構造、メタデータ、静的コンテンツ、最上位のレイアウト構成に Astro を使用します。状態を持つ部分やインタラクティブな部分には React アイランドを使用します。Astro から React へ渡すプロパティは、小さくシリアライズ可能なものにしてください。大きなサーバーオブジェクトをアイランドに渡さないでください。 Astro ページでは、ロケール、i18n、認証/セッション、アプリ管理のパスビルダーなどのリクエストコンテキストに Astro.locals を使用できます。フォームやインタラクティブなフローには React アイランドを使用できます。内部送信専用の公開 API を作成するのではなく、サーバーワークフローは Astro Actions、ルートハンドラー、またはコントローラーに保持してください。

Next 形式のアプリでは、サーバー専用コードをサーバーコンポーネント、ローダー、ルートハンドラー、またはアクションに保持します。クライアントコンポーネントはインタラクションとブラウザーの状態に集中させます。誤ってクライアントコンポーネントからサーバー専用モジュールをインポートしないでください。

コンポーネント

コンポーネントでは、レンダリングとローカルなインタラクションを担当します。

  • マークアップ、アクセシビリティ、レイアウト、視覚的な状態。
  • 開閉、入力中の値、選択中のタブ、ホバー、フォーカス、開示状態など、コンポーネント内の状態。
  • 提供されたコールバックや用途を絞ったフックの呼び出し。
  • 読み込み中、空、エラー、無効、保留中の各状態の表示。

その責務の境界を担うことが明示されている場合を除き、コンポーネントに永続的なサーバー状態の契約、機能をまたぐキャッシュポリシー、分析方針、ベンダー SDK のセットアップを担当させないでください。

次の分担を推奨します。

  • 共有 UI コンポーネント:構造、スタイル、アクセシビリティ、バリエーション、安定した DOM 契約。
  • 共通アプリコンポーネント:再利用可能なアプリシェル、または機能をまたぐ構成。
  • 機能コンポーネント:ドメインの動作、クエリーフック、ミューテーション、ルート固有の条件。
  • サービス/フック/ストア:API 呼び出し、クエリーオプション、キャッシュの変更、共有クライアント状態、外部ブラウザー連携。

共有コンポーネントにアプリとの接続処理が必要な場合は、ラッパー/コントローラーコンポーネントを使用します。コントローラーでは、データの取得、コンテキストの読み取り、ブラウザー負荷の大きい子コンポーネントの遅延読み込み、ドメイン値から UI プロパティへの変換、レンダースロットの受け渡しを行えます。こうしたアプリとの接続処理は共有コンポーネントに含めないでください。

コントローラーとプリミティブの構成:

export function ResourcePickerController(): React.JSX.Element {
  const { data = [] } = useQuery(resourceListQueryOptions)
  const create = useCreateResourceMutation()

  return (
    <ResourcePicker
      resources={data}
      onCreate={(name) => create.mutateAsync({ name })}
      renderEmpty={() => <EmptyState title="No resources yet" />}
    />
  )
}

export function ResourcePicker({
  resources,
  onCreate,
  renderEmpty,
}: {
  resources: ResourceSummary[]
  onCreate: (name: string) => Promise<void>
  renderEmpty: () => React.ReactNode
}): React.JSX.Element {
  if (resources.length === 0) return <>{renderEmpty()}</>
  return <ResourceList items={resources} onCreate={onCreate} />
}

検索可能なセレクト、コマンドメニュー、オートコンプリート、ファイルアップロード、編集可能なテキストでは、次の点に従います。

  • キーボード操作、フォーカス、選択、ARIA には、実績のあるインタラクションプリミティブまたはヘッドレスフックを優先します。
  • フィルタリング、配置、制御/非制御状態はフックに保持します。
  • DOM マークアップとスタイルは、Root、Input、Label、List、Item など、明示的なスロットを持つプリミティブに保持します。
  • 該当する場合は、Escape、Enter、矢印キー、フォーカス、無効状態、ラベル、説明、スクリーンリーダー専用テキストをサポートします。
  • ブラウザー専用の計測やグローバル変数は、イベントハンドラーまたはクライアントエフェクトの内側に置きます。

共通 UI パッケージ

機能やアプリ間で再利用され、視覚的または操作上のパターンをカプセル化し、アプリ固有のデータモデル、ルート、分析、API クライアントに依存しないコードは、共通 UI パッケージに配置します。

共通 UI パッケージのコンポーネントは、シンプルで組み合わせやすいものにします。

  • React の関数コンポーネントを使用します。DOM へのアクセスやプリミティブの合成に必要な場合は、forwardRef を使用します。
  • ブール値には is* または has*、コールバックには on* を使ったプロパティ名を使用します。
  • プロパティの数は少なく保ちます。レンダーコンテンツ用のプロパティが多数必要な場合は、コンポジションに切り替えます。
  • primary、secondary、danger、ghost、muted、sm、md、lg など、意味の明確なバリアントを使用します。
  • 利用側で本当に必要な場合に限り、バリアントをエクスポートします。それ以外の場合は、コンポーネントをエクスポートします。
  • キーボード操作、フォーカス管理、ラベル、ARIA、無効状態を正しく扱います。
  • 利用側でスタイルを失わずにレンダーされる要素を指定する必要がある場合は、asChild またはスロットによるコンポジションを使用します。
  • 呼び出し側があらゆる場所で ID を引き回さなくて済むよう、ラベル、説明、エラー領域には生成された ID を使用します。

名前付きの領域を持つ再利用可能なレイアウトには、複合コンポーネントが適しています。

type PanelProps = {
  children: React.ReactNode
  className?: string
}

function PanelRoot({ children, className }: PanelProps): React.JSX.Element {
  return <section className={cn("rounded-md border bg-card", className)}>{children}</section>
}

function PanelActions({ children, className }: PanelProps): React.JSX.Element {
  return <div className={cn("flex items-center gap-2", className)}>{children}</div>
}

export const Panel = Object.assign(PanelRoot, { Actions: PanelActions })

子コンポーネントを使うことで呼び出し側がより明確になる場合、汎用コンテナーに buttonLabel、actionSlotClassName、isSpecialFeatureMode のような一度限りのプロパティを追加しないでください。

データ取得とサーバー状態

サーバー状態はサーバー状態として扱います。ストアが独立したクライアントワークフローを表す場合を除き、クライアントストアに複製しないでください。

基本パターン:

  • API データには、リポジトリのサーバー状態ライブラリを使用します。通常は TanStack Query、tRPC の React Query 統合、またはリポジトリが提供する SWR/nanoquery ラッパーです。
  • ドメインまたは機能ごとのサービスモジュールに、クエリキーとクエリオプションを集約します。
  • リクエスト関数は小さく、型付けしてください。型付けされたフェッチャー、生成済みクライアント、tRPC プロシージャ、またはアプリが提供する API アダプターを呼び出すようにします。
  • コンポーネントではクエリオプションまたはサービスフックを使用し、文字列キーをあちこちで直接組み立てないようにします。
  • ミューテーションではサーバーリクエストを実行し、キャッシュを適切な場所で明示的に更新または無効化します。
  • 次の処理が完了を待つ必要がある場合は mutateAsync を使用します。UI イベントを起点に処理を開始し、完了を待たない場合は mutate を使用します。

多くの機能が REST エンドポイントを直接呼び出す場合は、共有 API フェッチャーを作成します。認証情報、JSON ヘッダー、204 の処理、JSON の解析フォールバック、型付けされた HTTP エラーをこのフェッチャーで管理します。機能ファイルでは fetch の処理を繰り返すのではなく、型付けされた createFetcher または生成済みクライアントを呼び出します。

型付けされたクエリオプションを使用すると、キー、フェッチャー、キャッシュポリシーをまとめて管理できます。

import { queryOptions } from "@tanstack/react-query"

export const resourceListQueryKey = ["resources", "list"] as const
export const resourceDetailQueryKey = (id: string) => ["resources", "detail", id] as const

export const resourceListQueryOptions = queryOptions({
  queryKey: resourceListQueryKey,
  queryFn: fetchResources,
  staleTime: 60_000,
})

export const resourceDetailQueryOptions = (id: string | undefined) =>
  queryOptions({
    queryKey: resourceDetailQueryKey(id ?? ""),
    queryFn: () => {
      if (!id) throw new Error("Resource ID is required")
      return fetchResource(id)
    },
    enabled: Boolean(id),
    staleTime: 30_000,
  })

tRPC 形式のクライアントでは、生成済みのフックにクエリキーや無効化ヘルパーがすでに用意されている場合も、同じ所有関係を維持します。機能サービスフックで関連するプロシージャやキャッシュ更新をまとめてもかまいませんが、無関係な関心事まで含む隠れたビジネス層にならないようにします。

アクション形式の API では、サービスオブジェクトにキャッシュキー、フェッチャーストア、ミューテーションメソッド、小さなワークフローヘルパーをまとめられます。一貫性のある形にしてください。

export const resourceService = {
  keys: {
    list: () => ["resources"],
    detail: (id: string | null) => ["resource", String(id)],
  },
  useList: () => useLiveQuery($resources, []),
  async update(id: string, payload: UpdateResourceInput) {
    const { data, error } = await actions.resource.update({ id, ...payload })
    if (error) throw error
    mutateCache(resourceService.keys.detail(id), data)
    revalidateKeys(resourceService.keys.list())
    return data
  },
}

これらのサービスが、無関係な機能を寄せ集めたものにならないようにしてください。サービスにキャッシュ操作、API 呼び出し、派生セレクターをまとめる場合は、すべてが同じユーザーワークフローまたはドメインに属している必要があります。

キャッシュの更新:

  • データの形が広範囲にわたる場合や、更新が複数のビューに影響する場合は、ミューテーション後に無効化する方法を優先します。
  • UI をすぐに修正する場合、詳細ビューと一覧ビューを同期する場合、既知の小さな更新を行う場合は、対象を絞った setQueryData を優先します。
  • 切り替え、名前変更、並べ替え、小さな編集、ローカルで完結するような操作には、高速な UX を実現するために楽観的更新を使用します。
  • 楽観的更新では、影響を受けるクエリをキャンセルし、以前の値を保存してから、表示中のすべてのキャッシュエントリーを更新します。エラー時には元に戻し、処理の確定時に無効化します。
  • アナリティクス、トースト、ナビゲーションは、低レベルのフェッチャー内ではなく、UI または機能の境界にあるミューテーションコールバックに置きます。
  • ミューテーションが検索、一覧、詳細、派生カウント、外部キャッシュのビューに影響する場合は、表示中のすべてのキャッシュを意図的に更新または無効化します。詳細キャッシュを1つ更新すれば、すべての画面が直ると考えないでください。

想定されるリクエスト失敗は、UI で表示できる型付きエラーまたは結果形式で通知します。生の JSON や通信エラーをそのまま UI に表示しないでください。

楽観的ミューテーションの形式:

const renameMutation = useMutation<Resource, Error, string, { previous?: ResourceList }>({
  mutationFn: (name) => updateResourceName({ id, name }),
  onMutate: async (name) => {
    await queryClient.cancelQueries({ queryKey: resourceListQueryKey })
    const previous = queryClient.getQueryData<ResourceList>(resourceListQueryKey)
    queryClient.setQueryData<ResourceList>(resourceListQueryKey, (current) =>
      current ? current.map((item) => (item.id === id ? { ...item, name } : item)) : current,
    )
    return { previous }
  },
  onError: (error, _name, context) => {
    if (context?.previous) queryClient.setQueryData(resourceListQueryKey, context.previous)
    toast.error(error.message)
  },
  onSuccess: (resource) => {
    queryClient.setQueryData(resourceDetailQueryKey(resource.id), resource)
    track("resource:update_name")
  },
  onSettled: () => {
    void queryClient.invalidateQueries({ queryKey: resourceListQueryKey })
  },
})

アプリの境界で、通常は useState(() => new QueryClient()) を使用して、安定したインスタンスをクエリクライアントに設定します。テストでは、再試行を無効にした決定的なクエリクライアントを使用します。

クライアント状態

コンポーネント内で完結する状態には、ローカルな React 状態を使用してください。状態が離れたコンポーネント間で共有される場合、ルート変更後も保持される場合、モーダルやエディターパネルを連携させる場合、または明示的なストアアクションが役立つ場合に限り、クライアントストアを使用してください。

ストアに関するガイダンス:

  • リポジトリでその規約を使用している場合は、nanostores の前に $ を付けてください。
  • ストア API は意図的に設計してください。ロジックが単純でない場合は、生のセッターではなく、open、close、reset、select、setDraft、mutateCache を公開してください。
  • 関連するストアが 1 つの UI ワークフローを表す場合は、1 つのファイルにまとめてください。
  • ストアのラッパーがリポジトリで選択されているサーバー状態キャッシュでない限り、サーバーデータはサーバー状態キャッシュに保持してください。
  • レンダリング中に動的な入力からフェッチャーストアを作成する場合は、useMemo でメモ化するか、購読する前に useRef に保持してください。レンダリングごとに新しいストアインスタンスを作成すると、再購読ループが発生することがあります。
  • SSR の影響を受けるストアでは、初期値をサポートし、ハイドレーションの不一致を避けるためにマウント後に購読してください。
  • データの取得が成功したら、古いエラーをクリアしてください。

ストアは、モーダルサービス、エディター状態、現在のタブセット、選択中の ID、アクティブなパネル、ローカルの下書きの連携、アプリレベルの設定に適しています。API の契約を隠したり、クエリデータをデフォルトで複製したりするためのものではありません。

エディターのような状態の場合:

  • 型付けされたデフォルト設定を用意し、所有するルートまたはリソースが変更されたときにそこへリセットしてください。
  • dirty フラグを追跡し、ストアのリスナーが読み込み直後のサーバー状態を書き戻してループを発生させないようにしてください。
  • 編集頻度の高い場合は、バックグラウンド保存をデバウンスしてください。
  • モジュールレベルのリスナーを通じて実行されるストアの副作用は、typeof window !== "undefined" でガードしてください。
  • 検証用の派生フックが同じ候補やキャッシュの状態に依存する場合は、ストアの近くに置いてください。

dirty なエディターストアの構造:

const $editor = map<EditorState>({
  dirty: false,
  config: structuredClone(DEFAULT_CONFIG),
})

const saveConfig = debounce((config: EditorConfig) => {
  void actions.resourceConfig.update({ config }).then(() => previewStore.invalidate())
}, 100)

$editor.listen((state) => {
  if (typeof window === "undefined") return
  if (!state.dirty) return
  saveConfig(state.config)
})

export const editorService = {
  useState: () => useStore($editor),
  reset: () => $editor.set({ dirty: false, config: structuredClone(DEFAULT_CONFIG) }),
  updateConfig: (config: EditorConfig) => $editor.setKey("config", config),
  markDirty: () => $editor.setKey("dirty", true),
}

UX 状態を永続化する場合は、saveDraft、getDraft、clearDraft のようなサービスメソッドの背後に、小さな名前付き Cookie、ローカルストレージヘルパー、または永続化アトムを使用してください。ストレージキーをコンポーネント内に散在させないでください。

フォームと契約

フォームでは、境界検証に正規のスキーマまたは DTO を使用します。共有スキーマや生成済みの契約がある場合、バックエンドのルールを模倣するフロントエンド用バリデーターを独自に作成しないでください。スキーマが存在せず、対象範囲内であれば、まず正規の契約レイヤーに追加します。

フォームに関するガイドライン:

  • リポジトリのフォームライブラリとフィールドコンポーネントを使用します。ラベル、説明、エラー、aria-invalid を通じてフィールドのアクセシビリティを確保します。
  • フォームごとに送信フローは 1 つにします。永続化を伴うミューテーションは onSubmit に配置します。
  • 値のトリミングと正規化は、入力欄に分散させず、送信境界で行います。
  • UX 上、任意項目となるフィールドでは、任意入力や空文字列の処理を一度だけ組み立て、再利用します。
  • フォーカスアウト時に自動保存する場合は、フィールドからフォーカスを外し、検証し、送信するという流れを明示します。
  • 入力変更時に自動保存する場合は、フォームライブラリのデバウンス機能か、安定したデバウンスコールバックを使用します。
  • 送信処理中は送信ボタンを無効にし、重要な操作には簡潔なローディング表示を行います。
  • 共有フィールドコンポーネントが想定する形式で、フィールドの近くにエラーを表示します。
  • サーバーアクションやフォーム送信で、状態管理、クリーンアップ、フィールドエラー、全般エラー、成功通知のフローがアプリ内で繰り返される場合は、再利用可能なアクションハンドラーを使用します。
  • サーバーアクションを使う構成では、内部フォームのためだけに公開 API ルートを新設せず、サーバーのワークフローはアクションやコントローラーに配置します。
  • Astro 形式のアプリでは、フォームに React アイランドを使用できます。周辺のアプリですでに Astro Actions を使用している場合は、それを通じて送信します。
  • Astro Actions では、catch の動作が同じなら、1 つの包括的な try/catch を優先します。明示的なアクションエラーは保持し、予期しないエラーはアプリ共有の getErrorMessage 形式のユーティリティで変換します。
  • ファイル入力やドロップゾーンでは、コンポーネント境界でスキーマを使ってファイルのメタデータを検証し、onBeforeValidation、onValidationError、onFileSelect コールバックを公開します。
  • 独自のスタイルを適用したフォームコントロールでも、ラベル、aria-describedby、aria-invalid、キーボード操作、非表示のネイティブ入力、状態変更時に必要なライブリージョンを含め、アクセシビリティを確保します。

簡潔なパターン:

const optionalNameSchema = z.union([z.literal(""), ResourceSchema.shape.name])

export function ResourceForm({
  defaultValues,
  onSubmit,
}: {
  defaultValues?: { name?: string }
  onSubmit: (values: { name?: string }) => Promise<void>
}): React.JSX.Element {
  const form = useForm({
    defaultValues: { name: defaultValues?.name ?? "" },
    onSubmit: async ({ value }) => {
      await onSubmit({ name: value.name.trim() || undefined })
    },
  })

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault()
        void form.handleSubmit()
      }}
    >
      <form.Field name="name" validators={{ onBlur: optionalNameSchema, onSubmit: optionalNameSchema }}>
        {(field) => (
          <input
            id={field.name}
            name={field.name}
            value={field.state.value}
            onBlur={field.handleBlur}
            onChange={(event) => field.handleChange(event.target.value)}
            aria-invalid={field.state.meta.errors.length > 0}
          />
        )}
      </form.Field>
    </form>
  )
}

フレームワークと統合モジュール

フロントエンドアプリには、分析、機能フラグ、認証セッションクライアント、ルートの書き換え、リクエストログ、ブラウザー専用 SDK、サーバーレンダリングアダプターなどの統合コードが含まれることがよくあります。これらは境界として扱います。

  • サードパーティ製のブラウザー SDK は、アプリが所有するサービス、プロバイダー、またはフックの背後にラップします。
  • React コンポーネント内ではプロバイダー/フックを使用します。React の外では、リポジトリですでにその分離に対応している場合に限り、シングルトンサービスを使用します。
  • ユーザーが開始したミューテーションの結果は onSuccess で、失敗は onError で追跡します。製品が試行された操作の追跡を明示的に求めていない限り、楽観的な onMutate では追跡しないでください。
  • 読み取り専用のクエリでは、デフォルトで分析イベントを送信する必要はありません。
  • 一貫したイベント名の形式を使用します。project:create や settings:update_name のように、domain:action 形式とスネークケースのアクション名を優先します。
  • 分析イベントには、関連性があり、機密情報を含まないコンテキストだけを渡します。
  • 分析のコンテキストに個人を特定できる情報や機密性の高いペイロードを決して含めないでください。
  • 統合モジュールは冪等性を保ちます。開発サーバーや HMR により、モジュールが再評価される場合があります。
  • ブラウザー専用のグローバルやサーバー専用モジュールには、実行環境のガードを使用します。
  • 認証/セッションのガードは、ルートやレイアウトの境界として維持します。各機能コンポーネントに認証チェックを散在させるのではなく、読み込み中、再試行/エラー、リダイレクト、ポリシー承認済みの各状態を明示的に描画します。
  • SSR データについては、可能な場合、正確な初期データをクライアントクエリに渡します。初回描画やメタデータに影響しない、クライアント専用の後続クエリでは SSR を無効にします。
  • ライブラリでフックへのアクセスが必要だが DOM は描画しない場合は、サーバーで計算した状態とサードパーティ製クライアントライブラリを同期するため、小さな「仮想」コンポーネントを使用します。
  • 重いブラウザー専用ウィジェットはコントローラーコンポーネントから遅延読み込みし、最小限のローディング状態を用意します。

設定駆動型 UI

チャート、エディター、ダッシュボード、フィルタービルダー、生成フォームなどの複雑な UI 領域では、条件分岐をあちこちに散在させるのではなく、型付きの設定レジストリを優先してください。

優れた設定駆動型 UI には、次の要素があります。

  • 型キーをレンダラー、フォームエディター、パーサー関数、またはランタイムビルダーに対応付ける一元的なレジストリ。
  • 安定した作成動作を実現する、型付きのデフォルト設定オブジェクト。
  • 設定とデータをランタイムオブジェクトに変換する純粋な変換関数。
  • 変換関数、型強制ルール、デフォルト値、無効な設定の動作を対象とした、的を絞ったテスト。
  • サポートされていない組み合わせでは、壊れた UI をレンダリングするのではなく、null または型付きの失敗を返すためのエスケープハッチ。
  • 生成されたエディターフィールドを命名規則でグループ化するのは、その規則がコード内に記載され、動作を通じてテストされている場合に限ります。
  • デフォルト値と型強制ルールは一元管理してください。UI フィールドごとに、同じ設定値の解釈方法を独自に定義してはいけません。

レジストリと変換の構成例:

const widgetConfig = {
  table: [TableEditor, buildTableWidget],
  chart: [ChartEditor, buildChartWidget],
  metric: [MetricEditor, buildMetricWidget],
} as const

export type WidgetType = keyof typeof widgetConfig
export type WidgetConfigValue = { id: string; type: WidgetType; title?: string }

export function buildWidget(config: WidgetConfigValue, data: ResultData): Widget | null {
  const entry = widgetConfig[config.type]
  if (!entry) return null
  const [, build] = entry
  return build(config, data)
}

不変条件、型強制ルール、分かりにくい実行順序にはコメントを付けてください。自明な JSX の仕組みにはコメントを付けないでください。

スタイル設定

リポジトリのスタイル設定システムとデザイントークンを使用してください。

  • 任意の色ではなく、bg-card、text-foreground、border-input などのセマンティックトークンや、同等のローカルトークンを優先してください。
  • 条件付きクラスには、Tailwind ユーティリティを cn() またはリポジトリのマージヘルパーと併用してください。
  • Astro コンポーネントでは、class:list またはフレームワーク標準の同等機能を使用してください。
  • サイズ、トーン、密度、状態のバリエーションには、cva または既存のバリアントヘルパーを使用してください。
  • cn() の呼び出しが長くなったり条件分岐が多くなったりした場合は、バリアント定義またはより小さなコンポーネントに切り出してください。
  • ランタイム値をトークンやクラスで表現できない場合を除き、インラインスタイルは避けてください。
  • レスポンシブ動作はモバイルファーストにし、既存のブレークポイントに合わせてください。
  • 視認できるフォーカス状態とキーボード操作の利便性を維持してください。

ファイルと命名規則

まずリポジトリの命名規則に従ってください。判断が難しい場合は、次の規則を優先してください。

  • ESM のインポートを使用し、型をインポートする際は型専用インポートを使用してください。
  • リポジトリがサポートしている場合、フロントエンド関連のツールやサーバー側のフロントエンドコードでは、Node の組み込みモジュールに node: プロトコルを使用してください。
  • インポートの順序は、React/フレームワークと外部ライブラリ、共有 UI/デザインシステムのパッケージ、アプリ/パッケージのエイリアス、相対インポート、型インポートの順にしてください。フォーマッターやリンターでより厳密な順序が指定されている場合は、それに従ってください。
  • ファイル名にはケバブケースを使用してください:resource-card.tsx、settings-dialog.astro、query-service.ts。
  • 役立つ場合は、ファイル名の末尾に種別を付けてください:.store.ts、.service.ts、.provider.ts、.module.ts、.test.tsx、.route.ts。
  • React コンポーネントは、ケバブケースのファイルからパスカルケースのエクスポート名で公開してください。
  • 機能ごとのフォルダーに、その機能固有のコンポーネント、フック、ストア、テストを配置してください。
  • 共有 UI パッケージに、汎用プリミティブと組み合わせ型のインタラクションパターンを配置してください。
  • リポジトリですでにバレルファイルを利用している場合を除き、バレルファイルは避けてください。直接インポートすることで、所有関係が明確になります。
  • 複数のアプリ、パッケージ、または機能で現実的に再利用できる場合に限り、共有の再利用可能なプリミティブを共有パッケージに配置してください。
  • 小さな関数を一つだけ置くためのヘルパーファイルは作成しないでください。呼び出し元が分かりやすくなる場合や、実際の重複を防げる場合を除き、小さなロジックはローカルにインラインで記述してください。

一般的なフロントエンドの構成には、components/feature-name、components/common、services/domain、stores、hooks、layouts、pages、routes、utils、integrations などがあります。大切なのは、フォルダー名を厳密にそろえることではなく、所有関係を明確にすることです。

テスト

型システムですでに保証されている内容ではなく、動作に焦点を当てたテストを優先してください。

通常は Vitest、Testing Library、Playwright、ローカルのレンダリングヘルパーなど、リポジトリで使用している技術スタックを使ってください。 i18n、ルーティング、クエリークライアント、認証/セッションなど、コンテキストに依存する内容には、実際のプロバイダー設定を使用してください。特に i18n では、テスト内で翻訳の動作を再実装するのではなく、実際の設定またはデフォルトのロケールを使用してください。

テスト対象:

  • ユーザーに表示される描画、操作、ナビゲーション、読み込み中、空、エラーの各状態。
  • 重要な条件分岐やエッジケース。
  • 回帰が起きやすい場合のクエリー/サービスのキャッシュ動作。
  • 楽観的更新、ロールバック、無効化、ミューテーションに伴う副作用。
  • フォーム:検証、正規化、無効状態、送信フロー、フィールドエラー。
  • 純粋な設定変換と、UI を生成するビルダー。
  • メニュー、ダイアログ、タブ、コンボボックス、複合ウィジェットの、アクセシビリティ上重要なキーボード操作。

避けること:

  • 大規模なスナップショット。
  • セマンティクスで十分な場合に、実装の詳細やクラス文字列そのものをテストすること。
  • パッケージ設定でインポートされていることを確認せずに、jest-dom のマッチャーが使えると想定すること。
  • describe を 3 階層以上ネストすること。
  • TypeScript ですでに保証されている静的型、プロパティの形状、または発生し得ない状態を再テストすること。

プロバイダーを必要とするコンポーネントには、クエリークライアント、ルーター、ストア、テーマ、フォームコンテキスト用の最小限のテストラッパーを作成してください。テストでは決定的なクエリークライアントを使用してください。リトライを無効にし、キャッシュを制御し、明示的にクリーンアップしてください。

ヘッドレスフックや操作の多いコンポーネントには、フックの内部をモックするのではなく、小さなテスト用ハーネスコンポーネントを作成してください。userEvent で DOM を操作し、表示内容、選択された値、キャッシュへの影響、またはコールバックの呼び出しを検証してください。

判断に迷った場合

  • 変更はできるだけ小さく、局所的にしてください。
  • 新しい抽象化を作る前に、既存の機能パターンを再利用してください。
  • 大きなファイルに場当たり的な分岐を追加する前に、目的を絞ったコンポーネント、フック、またはサービスを切り出してください。
  • 共有 UI は汎用的に保ち、プロダクト固有の動作は機能コンポーネント側に置いてください。
  • 契約とデザイントークンを信頼できる唯一の情報源にしてください。

コメント