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
