react-craft
npx skills add https://github.com/zainzafar90/skills --skill react-craft
Agent 安装分布
Skill 文档
React Craft
Naming, structure, testing, and readability for React components.
This skill covers naming, structure, testing, and readability. State management, hooks discipline, composition patterns, and UI components are handled by companion skills. Install them alongside this one â the agent will pick them up automatically based on task context.
# State & hooks (57 rules â derived state, useEffect misuse, waterfalls, bundle size) npx skills add vercel-labs/agent-skills --skill vercel-react-best-practices # Composition (compound components, avoid boolean prop sprawl, state lifting) npx skills add vercel-labs/agent-skills --skill vercel-composition-patterns # State management decision framework (Zustand vs RTK vs React Query vs Jotai) npx skills add wshobson/agents --skill react-state-management # TDD workflow npx skills add obra/superpowers --skill test-driven-development # shadcn/ui (Radix + Tailwind component patterns, forms w/ RHF + Zod) npx skills add giuseppe-trisciuoglio/developer-kit --skill shadcn-ui # Tailwind v4 + shadcn setup npx skills add jezweb/claude-skills --skill tailwind-v4-shadcn
1. Test First
Identify test cases BEFORE writing implementation. Co-locate tests next to components.
Button/
âââ Button.tsx
âââ Button.test.tsx
âââ index.ts
What to test (priority order)
- User behavior â clicks, typing, selection
- Conditional rendering â right elements for right state
- Edge cases â empty, error, loading, null, overflow
- Accessibility â keyboard nav, roles, labels
- Integration â works with parent/child context
Rules
- Test behavior, not implementation. Query by
role,label,textâ never by classname or test ID unless unavoidable. - One behavior per
it()block. - Descriptive names:
it('disables submit when form has validation errors')notit('works'). - No snapshot tests unless the markup is static and rarely changes.
// â
it('calls onSearch with trimmed query on submit', async () => {
const onSearch = vi.fn();
render(<SearchInput onSearch={onSearch} />);
await userEvent.type(screen.getByRole('searchbox'), ' react hooks ');
await userEvent.keyboard('{Enter}');
expect(onSearch).toHaveBeenCalledWith('react hooks');
});
// â
it('works', () => {
const { container } = render(<SearchInput onSearch={vi.fn()} />);
expect(container.querySelector('.search-input')).toBeTruthy();
});
2. Naming
Every name should answer: “What is this and what does it do?”
Components â PascalCase, noun-based
| â | â | Why |
|---|---|---|
InvoiceTable |
Table1 |
Describes what it renders |
UserAvatarDropdown |
AvatarDD |
No abbreviations |
PaymentMethodForm |
Form |
Too generic |
Props â camelCase, intention-revealing
- Booleans:
is,has,should,canprefix - Events:
onprefix (component API) /handleprefix (internal) - Render slots:
renderprefix orContentsuffix
// â
interface ProductCardProps {
product: Product;
isOnSale: boolean;
hasReviews: boolean;
onAddToCart: (id: string) => void;
renderBadge?: (product: Product) => ReactNode;
}
// â
interface ProductCardProps {
data: any;
sale: boolean;
click: () => void;
}
Hooks â always use + capability description
| â | â |
|---|---|
useDebounce(value, delay) |
useDb(v, d) |
usePaginatedProducts(filters) |
useData(filters) |
Handlers & variables
- Handlers:
handle+ noun + verb âhandleFormSubmit,handleRowClick - Booleans:
isLoading,hasError,shouldRefetch - Transforms:
formatPrice,parseQueryParams,toSlug - Collections: plural â
users,cartItems,selectedIds
Files â kebab-case everywhere
All files use kebab-case. No exceptions. Consistency aids grep and discovery across the codebase.
| Type | Convention | Example |
|---|---|---|
| Components | kebab-case.tsx |
order-list-table.tsx |
| Hooks | use-*.ts |
use-order-table-query.ts |
| Utils | kebab-case.ts |
format-currency.ts |
| Constants | kebab-case.ts |
route-paths.ts |
| Types | kebab-case.ts |
order-types.ts |
| API layer | *.requests.ts |
orders.requests.ts |
| API hooks | *.hooks.ts |
orders.hooks.ts |
3. Component Structure
Anatomy (consistent order inside every component)
// 1. Types (if co-located)
interface WidgetProps { ... }
// 2. Constants scoped to component
const TREND_ICONS = { up: TrendUp, down: TrendDown } as const;
// 3. Component
export function Widget({ title, metric, trend }: WidgetProps) {
// 3a. Hooks (top, consistent order)
const [isExpanded, setIsExpanded] = useState(false);
const formatted = useMemo(() => formatNumber(metric), [metric]);
// 3b. Derived values
const TrendIcon = TREND_ICONS[trend];
// 3c. Handlers
const handleToggle = () => setIsExpanded(prev => !prev);
// 3d. Early returns (guards)
if (!metric) return <EmptyState title={title} />;
// 3e. Render
return ( ... );
}
Readability rules
- ~150 lines max per component. Extract sub-components or hooks if longer.
- 5-7 props max before considering composition/compound patterns.
- Destructure props in the signature, not the body.
- One component per file (small co-located helpers are OK).
- Early returns for loading/error/empty â no nested ternary soup.
// â Ternary hell
{isLoading ? <Spinner /> : error ? <Error /> : data?.length ? data.map(...) : <Empty />}
// â
Guard clauses
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage message={error} />;
if (!data?.length) return <EmptyState />;
return <div>{data.map(...)}</div>;
4. Project Structure
Scale to project size. Don’t over-architect small projects.
Small:
src/
âââ components/
âââ hooks/
âââ utils/
âââ types.ts
âââ App.tsx
Medium/Large â Route-colocated architecture:
src/
âââ components/ # Shared/reusable UI only
â âââ data-table/ # One concern per file â never mix unrelated components
â âââ project-filter/
â âââ ...
âââ routes/
â âââ orders/
â âââ order-list/
â âââ order-list.tsx # Page â orchestration only (~60-80 LOC)
â âââ hooks/
â â âââ use-order-table-query.ts
â âââ components/
â â âââ order-list-table.tsx
â âââ index.ts
âââ hooks/ # Shared hooks only
âââ lib/
â âââ client/
â âââ orders.requests.ts # Resource-scoped API layer
âââ providers/
â âââ router/
â âââ route-map.tsx # Lazy route modules
âââ utils/
âââ types/
âââ constants/
Route page files are orchestrators, not containers
A page file wires together hooks and components. It should not contain table configs, query logic, or form schemas.
// â
~60 LOC â orchestration only
export function OrderList() {
const { data, isLoading } = useOrderTableQuery();
const filters = useOrderFilters();
if (isLoading) return <PageSkeleton />;
return (
<PageLayout title="Orders">
<OrderFilters {...filters} />
<OrderListTable data={data} />
</PageLayout>
);
}
// â 400+ LOC page with inline table columns, filter logic, query params
File size discipline
Target ~60 LOC avg per file. Hard ceiling of 200 LOC â if you hit it, split.
| LOC | Action |
|---|---|
| < 100 | Good |
| 100â200 | Review â can a hook or sub-component be extracted? |
| 200+ | Must split. Extract hooks/, components/, or utils/ next to the file. |
Lazy route loading
Always use lazy imports in the router. Static imports bundle every page upfront.
// â
{ path: "/orders", lazy: () => import("@/routes/orders/order-list") }
// â
import { OrderList } from "@/routes/orders/order-list";
{ path: "/orders", element: <OrderList /> }
Barrel export scoping
- Feature barrels (
index.ts) expose only the public API of that feature. - Never create broad root barrels (e.g.,
lib/index.tsre-exporting everything) â they hurt tree-shaking and make implicit dependencies invisible. - If only one feature uses a util, it lives in that feature folder, not in shared
lib/.
// â
Direct import
import { formatOrderDate } from "@/routes/orders/utils/format-order-date";
// â Broad barrel
import { formatOrderDate } from "@/lib";
Principles
- Colocate related code (tests, hooks, components next to their page)
- Route-local hooks/ and components/ before promoting to shared
- No circular deps between route features
- Max 3 levels of nesting
- Kebab-case for all file names (consistency aids grep/discovery)
5. Anti-Patterns (Real-World)
These are patterns found in production codebases that hurt readability and reviewability.
God files with mixed concerns
A 941-line data-table.tsx that contains both ProjectFilter and an unrelated DataTable. Split them â one concern per file, always.
Fat providers
A 460-line auth-provider.tsx doing data fetching, state management, and UI logic. Break into: use-auth.ts (hook) + auth-context.tsx (context) + auth-guard.tsx (UI boundary).
Flat route dumping
All pages in routes/pages/ with shared logic in routes/components/ means every page implicitly depends on the entire shared folder. Use route-colocated hooks/ and components/ instead.
Broad barrels hiding dependencies
A root lib/index.ts re-exporting everything makes it impossible to see what a feature actually depends on. Import directly from the source file.
Static router imports
Importing every page at the top of router.tsx bundles everything upfront. Use lazy: () => import(...) per route.
6. Accessibility Baseline
Every component must have:
- Semantic HTML:
<button>for actions,<a>for nav â never<div onClick> - Keyboard support: all interactive elements focusable, visible focus ring, Escape closes overlays
- ARIA when semantic HTML isn’t enough:
aria-labelon icon buttons,aria-livefor dynamic updates,aria-expandedfor toggles - Touch targets: min 44x44px on mobile
7. Pre-Ship Checklist
- Tests cover behavior, edge cases, a11y
- Names are self-documenting (component, props, hooks, handlers)
- Single responsibility â no “and” in the description
- File under 200 LOC â page files under 100
- No
anytypes - No
console.log - No magic strings/numbers â use named constants
- Loading, error, empty states handled
- Keyboard navigable
- Route is lazy-loaded
- No broad barrel imports â direct imports only