mcpField
R
react-frontend-expert

react-frontend-expert

github.com/hieutrtr開発8205 回インストール39 週間前に更新
リポジトリ

react-frontend-expert について

React と TypeScript の開発者向けスキルです。コンポーネント、フック、ルーティング、データ取得、フォームの実装に加え、型とアクセシビリティの指針を示します。

主な機能

  1. 01関数コンポーネントの規約
  2. 02カスタムフックのパターン
  3. 03TanStack Query によるデータ取得
  4. 04TypeScript の規約
  5. 05フォーム検証とアクセシビリティ

活用シーン

  1. 01React コンポーネントの作成・変更
  2. 02ルーティングを含むページの構築
  3. 03クエリ、変更、キャッシュ無効化の実装
  4. 04検証付きフォームの開発

インストール

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

$npx skills add https://github.com/hieutrtr/ai1-skills --skill react-frontend-expert

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

同梱ファイルreferences/component-templates.mdreferences/tanstack-query-patterns.md

詳細

React フロントエンドエキスパート

使用するタイミング

次の場合にこのスキルを有効にします:

  • React コンポーネントの作成または変更(関数コンポーネントのみ)
  • カスタムフック(useXxx)の作成
  • ルーティングを使用したページの構築
  • TanStack Query を使用したデータ取得の実装
  • バリデーションを含むフォームの処理
  • React/TypeScript アプリケーションのプロジェクト構成の設定

次の場合はこのスキルを使用しないでください:

  • コンポーネントまたはフックのテストの作成(react-testing-patternsを使用)
  • E2E ブラウザーテスト(e2e-testingを使用)
  • API コントラクトの設計(api-design-patternsを使用)
  • バックエンドの実装(python-backend-expertを使用)
  • デプロイまたは CI/CD(deployment-pipelineを使用)

手順

プロジェクト構成
src/
├── api/                  # API client functions and query options
│   ├── client.ts         # Axios/fetch instance with interceptors
│   ├── users.ts          # User API functions + query options
│   └── posts.ts
├── components/           # Shared, reusable UI components
│   ├── Button.tsx
│   ├── Modal.tsx
│   ├── Table/
│   │   ├── Table.tsx
│   │   └── TablePagination.tsx
│   └── Form/
│       ├── Input.tsx
│       └── Select.tsx
├── features/             # Domain-specific feature components
│   ├── users/
│   │   ├── UserList.tsx
│   │   └── UserProfile.tsx
│   └── posts/
│       └── PostEditor.tsx
├── hooks/                # Custom hooks
│   ├── useAuth.ts
│   ├── useDebounce.ts
│   └── usePagination.ts
├── layouts/              # Layout components
│   ├── MainLayout.tsx
│   └── AuthLayout.tsx
├── pages/                # Route-level page components
│   ├── HomePage.tsx
│   ├── LoginPage.tsx
│   └── users/
│       ├── UserListPage.tsx
│       └── UserDetailPage.tsx
├── types/                # Shared TypeScript types
│   ├── api.ts            # API response types
│   └── user.ts
├── App.tsx               # Root component with providers and router
└── main.tsx              # Entry point
コンポーネント構成
関数コンポーネントのみを使用
interface UserCardProps {
  user: User;
  onEdit: (userId: number) => void;
  showEmail?: boolean;
}

export function UserCard({ user, onEdit, showEmail = false }: UserCardProps) {
  return (
    <article className="user-card">
      <h3>{user.displayName}</h3>
      {showEmail && <p>{user.email}</p>}
      <button type="button" onClick={() => onEdit(user.id)}>
        Edit
      </button>
    </article>
  );
}

コンポーネントのルール:

  • 共有コンポーネントには名前付きエクスポートを使用します:export function Button
  • ページコンポーネントにはデフォルトエクスポートを使用します:export default function UserListPage
  • Props インターフェースの名前は{Component}Propsにします
  • 関数のシグネチャで props を分割代入します
  • コンポーネントは200行未満に保ちます。それを超える場合はサブコンポーネントまたはフックに切り出します
  • 深い階層の prop の受け渡しよりも、childrenとコンポジションを使用します
  • React.FCは使用せず、通常の関数構文を使用します
コンポーネントファイルの構成

複雑なコンポーネントでは、関連ファイルを同じ場所にまとめます:

UserProfile/
├── UserProfile.tsx       # Main component
├── UserProfile.css       # Styles (or .module.css)
├── UserAvatar.tsx        # Sub-component
└── index.ts              # Re-export: export { UserProfile } from './UserProfile'
フックのルールとカスタムフック
フックのルール
  1. フックはトップレベルでのみ呼び出します。ループ、条件分岐、ネストされた関数内では決して呼び出しません
  2. フックは React の関数コンポーネントまたはカスタムフックからのみ呼び出します
  3. カスタムフックの名前はuseで始めます
カスタムフックのパターン

useDebounce:

export function useDebounce<T>(value: T, delayMs: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delayMs);
    return () => clearTimeout(timer);
  }, [value, delayMs]);

  return debouncedValue;
}

useAuth:

interface AuthContext {
  user: User | null;
  isAuthenticated: boolean;
  login: (credentials: LoginCredentials) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthContext | null>(null);

export function useAuth(): AuthContext {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth must be used within AuthProvider");
  }
  return context;
}

usePagination:

interface PaginationState {
  cursor: string | null;
  hasMore: boolean;
  goToNext: (nextCursor: string) => void;
  reset: () => void;
}

export function usePagination(): PaginationState {
  const [cursor, setCursor] = useState<string | null>(null);
  const [hasMore, setHasMore] = useState(true);

  return {
    cursor,
    hasMore,
    goToNext: (nextCursor: string) => {
      setCursor(nextCursor);
    },
    reset: () => {
      setCursor(null);
      setHasMore(true);
    },
  };
}

カスタムフックに切り出すタイミング:

  • ロジックが2つ以上のコンポーネントで再利用される
  • コンポーネントの状態管理が複雑(useStateの呼び出しが3回を超える)
  • 副作用をカプセル化する必要がある(サブスクリプション、タイマー)
  • データ取得ロジックを共有できる
TanStack Query を使用したデータ取得
クエリーオプションファクトリー(推奨)

キーの衝突を防ぐため、クエリーキーと関数の定義を一元化します:

// api/users.ts
import { queryOptions } from "@tanstack/react-query";

export const userQueries = {
  all: () =>
    queryOptions({
      queryKey: ["users"],
      queryFn: () => apiClient.get<UserListResponse>("/users"),
    }),

  detail: (userId: number) =>
    queryOptions({
      queryKey: ["users", userId],
      queryFn: () => apiClient.get<UserResponse>(`/users/${userId}`),
    }),

  search: (query: string) =>
    queryOptions({
      queryKey: ["users", "search", query],
      queryFn: () => apiClient.get<UserListResponse>(`/users?q=${query}`),
      enabled: query.length > 0,
    }),
};
コンポーネントでのクエリーの使用
export function UserDetailPage({ userId }: { userId: number }) {
  const { data: user, isPending, isError, error } = useQuery(
    userQueries.detail(userId)
  );

  if (isPending) return <Spinner />;
  if (isError) return <ErrorMessage error={error} />;

  return <UserProfile user={user} />;
}
キャッシュ無効化を伴うミューテーション
export function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: UserCreate) =>
      apiClient.post<UserResponse>("/users", data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["users"] });
    },
  });
}

TanStack Query のルール:

  • staleTimeは0より大きい値に設定します(デフォルトの0は短すぎます):staleTime: 5 * 60 * 1000(5分)
  • ミューテーション後はinvalidateQueries()を使用します。手動のrefetch()は決して使用しません
  • すべての状態を処理します:isPending、isError、data
  • queryOptions()ファクトリーを使用します。キーのタイプミスや重複を防げます
  • パラメーターが不完全な状態でクエリーが実行されないよう、enabledを使用します
TypeScript の規約
// Use `interface` for object shapes (components props, API responses)
interface User {
  id: number;
  email: string;
  displayName: string;
  role: "admin" | "editor" | "member";
}

// Use `type` for unions, intersections, and computed types
type UserRole = User["role"];
type CreateOrUpdate = UserCreate | UserUpdate;

// Discriminated unions for state machines
type AsyncState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error };

TypeScript のルール:

  • tsconfig.jsonでstrict: trueを有効にします。例外はありません
  • anyは決して使用しません。本当に型が不明な場合はunknownを使用します
  • リテラルオブジェクト型にはas constを使用します
  • 拡張可能な型にはinterfaceを、それ以外にはtypeを優先します
  • 再利用可能なユーティリティ型やフックにはジェネリクスを使用します
  • 共有する型はtypes/ディレクトリからエクスポートします
フォームの処理
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const userSchema = z.object({
  email: z.string().email("Invalid email"),
  displayName: z.string().min(1, "Required").max(100),
  role: z.enum(["admin", "editor", "member"]),
});

type UserFormData = z.infer<typeof userSchema>;

export function UserForm({ onSubmit }: { onSubmit: (data: UserFormData) => void }) {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<UserFormData>({
    resolver: zodResolver(userSchema),
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <label htmlFor="email">Email</label>
      <input id="email" type="email" {...register("email")} aria-invalid={!!errors.email} />
      {errors.email && <span role="alert">{errors.email.message}</span>}

      <label htmlFor="displayName">Name</label>
      <input id="displayName" {...register("displayName")} aria-invalid={!!errors.displayName} />
      {errors.displayName && <span role="alert">{errors.displayName.message}</span>}

      <button type="submit" disabled={isSubmitting}>Save</button>
    </form>
  );
}
アクセシビリティ要件

すべてのコンポーネントは WCAG 2.1 AA に準拠する必要があります:

  1. セマンティックな HTML を優先: <div onClick>ではなく、<button>、<nav>、<main>、<article>を使用します
  2. ラベル: すべてのフォーム入力に、htmlForとidが一致する<label>を設定します
  3. ARIA は必要な場合のみ使用: アイコンのみのボタンにはaria-label、動的な更新にはaria-live、エラーにはrole="alert"を使用します
  4. キーボード操作: すべてのインタラクティブ要素に Tab キーで移動でき、Enter キーまたは Space キーで操作できるようにします
  5. フォーカス管理: ルート変更時にメインコンテンツへフォーカスを設定し、モーダル内ではフォーカスを閉じ込めます
  6. 色のコントラスト: 通常のテキストは最低4.5:1、大きなテキストは3:1にします
  7. 代替テキスト: すべての<img>タグに説明的なaltを設定します(装飾画像の場合はalt="")

例

検索とページネーションを備えたユーザーリストページ
export default function UserListPage() {
  const [search, setSearch] = useState("");
  const debouncedSearch = useDebounce(search, 300);
  const pagination = usePagination();

  const { data, isPending } = useQuery(
    userQueries.list({ q: debouncedSearch, cursor: pagination.cursor })
  );

  return (
    <main>
      <h1>Users</h1>
      <input
        type="search"
        value={search}
        onChange={(e) => { setSearch(e.target.value); pagination.reset(); }}
        placeholder="Search users..."
        aria-label="Search users"
      />
      {isPending ? <Spinner /> : (
        <>
          <UserTable users={data.items} />
          {data.hasMore && (
            <button onClick={() => pagination.goToNext(data.nextCursor)}>
              Load more
            </button>
          )}
        </>
      )}
    </main>
  );
}

エッジケース

  • フック内の古いクロージャー: 状態を参照するコールバックを使用する場合は、頻繁に変化する可変値には useRef を使用するか、useCallback/useEffect の配列に依存関係を含めてください。

  • TanStack Query のキーの衝突: キーは階層的に構成してください。リストには ["users"]、詳細には ["users", id]、フィルター付きリストには ["users", { q, page }] を使用します。キーの定義を一元管理するには、queryOptions() ファクトリーを使用してください。

  • 無限再レンダリング: よくある原因には、依存配列の欠落、レンダリング時に新しいオブジェクトや配列を作成すること(useMemo でラップしてください)、適切な条件を設けずに useEffect 内で状態を更新することがあります。

  • ハイドレーションの不一致: 初回レンダリング時に、ブラウザー専用 API(window、localStorage)に依存するコンテンツをレンダリングしないでください。useEffect を使用するか、typeof window !== "undefined" を確認してください。

  • メモリーリーク: useEffect のクリーンアップで非同期処理をキャンセルしてください。クエリでは TanStack Query が自動的に処理します。

注釈付きコンポーネントテンプレートについては references/component-templates.md を参照してください。 CRUD クエリのパターンについては references/tanstack-query-patterns.md を参照してください。

コメント