react
SKILL.md
React Guide
Applies to: React 18+, TypeScript, Single Page Applications, Component Libraries, Web Apps
Core Principles
- Component-Based: Build encapsulated components that manage their own state
- Declarative: Describe what UI should look like, React handles DOM updates
- Unidirectional Data Flow: Props down, callbacks up
- Composition Over Inheritance: Prefer component composition and custom hooks
- TypeScript First: All components use TypeScript with strict prop interfaces
Guardrails
Component Rules
- Use functional components exclusively (no class components)
- Keep components under 200 lines (split into smaller components)
- Every component has a TypeScript interface for props
- Use
React.ReactNodefor children props, notJSX.Element - Implement error boundaries for graceful error handling
- Use proper accessibility attributes (ARIA roles, labels, keyboard navigation)
- Co-locate component, styles, and tests in the same directory
Hooks Rules
- Only call hooks at the top level (never inside conditions, loops, or nested functions)
- Only call hooks from React function components or custom hooks
- Name custom hooks with
useprefix:useAuth,useQuery,useFetch - Always include all dependencies in
useEffect/useMemo/useCallbackarrays - Clean up side effects in
useEffectreturn function - Prefer
useReduceroveruseStatefor complex state logic - Use ESLint
react-hooksplugin to enforce rules
State Management
- Local state:
useStatefor simple,useReducerfor complex - Shared state: Context API for low-frequency updates (theme, auth, locale)
- Server state: TanStack Query (React Query) for data fetching and caching
- Global client state: Zustand for complex cross-component state
- Avoid prop drilling beyond 2 levels (use composition or context)
- Never store derived state (compute it during render)
Performance
- Use
React.memo()only for expensive pure components (measure first) - Use
useMemofor expensive computations,useCallbackfor stable references - Lazy load routes and heavy components with
React.lazy()+Suspense - Virtualize long lists (react-window, @tanstack/virtual)
- Keep bundle under 200KB initial load (code-split aggressively)
- Avoid anonymous functions in JSX for frequently re-rendered components
File Naming
- Components:
PascalCase.tsx(e.g.,UserCard.tsx) - Hooks:
camelCase.tswithuseprefix (e.g.,useAuth.ts) - Utils:
camelCase.ts(e.g.,formatDate.ts) - Types:
camelCase.tsortypes.tsper feature - Tests:
*.test.tsxor*.spec.tsx(co-located) - Styles:
*.module.cssor*.styles.ts(co-located)
Project Structure
my-app/
├── public/
│ └── index.html
├── src/
│ ├── assets/ # Static assets (images, fonts)
│ ├── components/ # Reusable components
│ │ ├── ui/ # Base UI components (Button, Input, Modal)
│ │ └── features/ # Feature-specific components
│ ├── hooks/ # Custom hooks
│ ├── contexts/ # React contexts
│ ├── services/ # API services
│ ├── utils/ # Utility functions
│ ├── types/ # TypeScript types
│ ├── pages/ # Page components (route-level)
│ ├── App.tsx
│ ├── main.tsx
│ └── index.css
├── tests/
├── package.json
├── tsconfig.json
├── vite.config.ts
└── README.md
components/ui/for reusable design system primitivescomponents/features/for domain-specific compositionshooks/for shared custom hooks (feature-specific hooks co-locate with component)contexts/for React context providersservices/for API layer (fetch wrappers, API clients)pages/for route-level components only
Component Patterns
Functional Component with Props
interface UserCardProps {
userId: string;
onSelect?: (user: User) => void;
className?: string;
}
export function UserCard({ userId, onSelect, className }: UserCardProps) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
try {
const data = await userService.getById(userId);
setUser(data);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
if (loading) return <Skeleton />;
if (!user) return null;
return (
<div className={cn('user-card', className)} onClick={() => onSelect?.(user)}>
<Avatar src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}
Component with Children
interface CardProps {
title: string;
children: React.ReactNode;
footer?: React.ReactNode;
}
export function Card({ title, children, footer }: CardProps) {
return (
<div className="card">
<div className="card-header"><h2>{title}</h2></div>
<div className="card-body">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
State Management Patterns
Context API (Auth, Theme, Locale)
interface AuthContextValue {
user: User | null;
login: (credentials: LoginCredentials) => Promise<void>;
logout: () => void;
isAuthenticated: boolean;
}
const AuthContext = createContext<AuthContextValue | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = async (credentials: LoginCredentials) => {
const user = await authService.login(credentials);
setUser(user);
};
const logout = () => {
authService.logout();
setUser(null);
};
return (
<AuthContext.Provider value={{ user, login, logout, isAuthenticated: !!user }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within AuthProvider');
return context;
}
TanStack Query (Server State)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: () => userService.getAll(),
staleTime: 5 * 60 * 1000,
});
}
function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateUserDTO) => userService.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}
Zustand (Complex Client State)
import { create } from 'zustand';
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
clearCart: () => void;
total: () => number;
}
const useCartStore = create<CartStore>()((set, get) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
removeItem: (id) => set((state) => ({
items: state.items.filter((item) => item.id !== id),
})),
clearCart: () => set({ items: [] }),
total: () => get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
}));
Routing (React Router v6)
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <Home /> },
{ path: 'dashboard', element: <Dashboard /> },
{ path: 'settings', element: <Settings /> },
{ path: '*', element: <NotFound /> },
],
},
]);
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<RouterProvider router={router} />
</Suspense>
);
}
Forms (React Hook Form + Zod)
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'),
password: z.string().min(8, 'Password must be at least 8 characters'),
name: z.string().min(2, 'Name is required'),
});
type UserFormData = z.infer<typeof userSchema>;
function UserForm({ onSubmit }: { onSubmit: (data: UserFormData) => void }) {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset,
} = useForm<UserFormData>({ resolver: zodResolver(userSchema) });
const handleFormSubmit = async (data: UserFormData) => {
await onSubmit(data);
reset();
};
return (
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div>
<label htmlFor="email">Email</label>
<input id="email" type="email" {...register('email')} />
{errors.email && <span className="error">{errors.email.message}</span>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
Testing Overview
Standards
- Use React Testing Library (
@testing-library/react) for component tests - Use
userEventoverfireEventfor realistic user interactions - Test behavior, not implementation details
- Query by accessible roles and labels first (not test IDs)
- Mock API calls, not internal component state
- Coverage target: >80% for business logic components
Basic Component Test
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('UserCard', () => {
it('renders user information', () => {
render(<UserCard user={mockUser} />);
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
it('calls onSelect when clicked', async () => {
const onSelect = vi.fn();
render(<UserCard user={mockUser} onSelect={onSelect} />);
await userEvent.click(screen.getByRole('button'));
expect(onSelect).toHaveBeenCalledWith(mockUser);
});
});
Tooling
Essential Commands
npm create vite@latest my-app -- --template react-ts # New project
npm install # Install deps
npm run dev # Dev server
npm run build # Production build
npm run lint # ESLint
npm test # Run tests
npm run test:cov # Coverage
Recommended Dependencies
{
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.x",
"@tanstack/react-query": "^5.x",
"zustand": "^4.x",
"react-hook-form": "^7.x",
"zod": "^3.x",
"@hookform/resolvers": "^3.x"
},
"devDependencies": {
"@types/react": "^18.x",
"@types/react-dom": "^18.x",
"@testing-library/react": "^14.x",
"@testing-library/user-event": "^14.x",
"vitest": "^1.x",
"eslint-plugin-react-hooks": "^4.x"
}
}
ESLint Configuration
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
"rules": {
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
Advanced Topics
For detailed patterns and examples, see:
- references/patterns.md -- Compound components, custom hooks, testing, performance, security patterns
External References
Weekly Installs
9
Repository
ar4mirez/samuelGitHub Stars
3
First Seen
Feb 20, 2026
Security Audits
Installed on
opencode9
gemini-cli9
github-copilot9
codex9
amp9
kimi-cli9