React — главная жертва «AI-кодинга наугад». Компоненты плодятся, хуки дублируются, styled-жопа копируется 20 раз. С Claude Code это лечится, но только при правильной настройке.

Канал с гайдами и контентом по claude code, выкладываем новости (когда режут лимиты в 10 раз) и какие инструменты через claude реализуем для проектов, канал: https://t.me/claudedevolper

Стартовый каркас

npm create vite@latest my-app -- --template react-ts
cd my-app && npm install

Потом в Claude:

Добавь:
- TanStack Query для серверного state
- React Hook Form + Zod для форм
- shadcn/ui для компонентов
- React Router v7
- Vitest + React Testing Library
- ESLint + Prettier + husky pre-commit
- Структура: src/{components,features,hooks,lib,api,pages}/

CLAUDE.md для React

## Стек
- React 19, Vite, TypeScript strict
- TanStack Query для API
- React Hook Form + Zod для форм
- shadcn/ui + Tailwind CSS

## Структура
- `src/features/<name>/` — фича (hooks, компоненты, типы) — инкапсулируется
- `src/components/ui/` — только shadcn-компоненты, не кастомные
- `src/components/<Name>.tsx` — переиспользуемые компоненты
- `src/api/<resource>.ts` — queries/mutations
- `src/lib/` — утилиты без React

## Правила
- Один компонент = один файл. Файл > 250 строк — декомпозировать
- Никаких `useEffect` для fetch — только TanStack Query
- Никаких `any`, никаких `@ts-ignore`
- Формы — только React Hook Form, never useState для полей
- Никаких inline-стилей. Tailwind или css-модуль

API-слой на TanStack Query

// src/api/users.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { z } from "zod";

const UserSchema = z.object({
  id: z.number(),
  email: z.string().email(),
  name: z.string(),
});

export type User = z.infer<typeof UserSchema>;

export const userKeys = {
  all: ["users"] as const,
  me: () => [...userKeys.all, "me"] as const,
};

export function useMe() {
  return useQuery({
    queryKey: userKeys.me(),
    queryFn: async () => {
      const res = await fetch("/api/v1/users/me", { credentials: "include" });
      if (!res.ok) throw new Error("Not authenticated");
      return UserSchema.parse(await res.json());
    },
  });
}

export function useLogin() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (creds: { email: string; password: string }) => {
      const res = await fetch("/api/v1/auth/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(creds),
      });
      if (!res.ok) throw new Error("Invalid credentials");
      return res.json();
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: userKeys.all }),
  });
}

Форма через React Hook Form + Zod

// src/features/login/LoginForm.tsx
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useLogin } from "@/api/users";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";

const schema = z.object({
  email: z.string().email("Некорректный email"),
  password: z.string().min(8, "Минимум 8 символов"),
});

type FormData = z.infer<typeof schema>;

export function LoginForm() {
  const login = useLogin();
  const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
    resolver: zodResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((data) => login.mutate(data))} className="space-y-4 max-w-sm">
      <div>
        <Input {...register("email")} placeholder="email" type="email" />
        {errors.email && <p className="text-red-500 text-sm">{errors.email.message}</p>}
      </div>
      <div>
        <Input {...register("password")} placeholder="password" type="password" />
        {errors.password && <p className="text-red-500 text-sm">{errors.password.message}</p>}
      </div>
      <Button type="submit" disabled={login.isPending}>
        {login.isPending ? "Вхожу..." : "Войти"}
      </Button>
      {login.isError && <p className="text-red-500">{login.error.message}</p>}
    </form>
  );
}

Тесты компонента

// src/features/login/LoginForm.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { LoginForm } from "./LoginForm";

function renderWithQuery(ui: React.ReactNode) {
  const qc = new QueryClient();
  return render(<QueryClientProvider client={qc}>{ui}</QueryClientProvider>);
}

describe("LoginForm", () => {
  it("показывает ошибку на невалидный email", async () => {
    renderWithQuery(<LoginForm />);
    await userEvent.type(screen.getByPlaceholderText("email"), "not-an-email");
    await userEvent.click(screen.getByRole("button"));
    expect(await screen.findByText(/Некорректный email/)).toBeInTheDocument();
  });
});

Подводные камни

  • Claude любит класть всё в один App.tsx. Жёстко требуй разнесения по features/.
  • useEffect(() => fetch(...)) — даже когда запрещено, модель срывается. Если видишь — заверни в TanStack Query.
  • shadcn-компоненты редактируют сами себя. Помни: они живут в твоём проекте, не в node_modules. Не трогай без необходимости.

Как попробовать

1. npm create vite@latest 2. Положи CLAUDE.md 3. Попроси первую фичу — «форма логина с валидацией» 4. Прогони тесты через npm run test 5. Деплой на Vercel: vercel → готово

Канал с гайдами и контентом по claude code, выкладываем новости (когда режут лимиты в 10 раз) и какие инструменты через claude реализуем для проектов, канал: https://t.me/claudedevolper