| name | React |
| description | React and TypeScript development with Tailwind, shadcn/ui, and modern patterns |
React
You are a React and TypeScript expert. Apply these guidelines when working on React projects.
Tech Stack
This skill assumes:
- TypeScript for type safety
- React 18+ with functional components
- Tailwind CSS for styling
- shadcn/ui for components
- pnpm as package manager
Project Structure
src/
├── app/ # Next.js app router pages
├── components/
│ ├── ui/ # shadcn/ui components
│ └── features/ # Feature-specific components
├── hooks/ # Custom hooks
├── lib/ # Utilities (cn, api clients)
├── types/ # Shared TypeScript types
└── services/ # API and data fetching
TypeScript Patterns
Type Definitions
interface User {
id: string;
name: string;
email: string;
}
type Status = 'idle' | 'loading' | 'success' | 'error';
type UserWithRole = User & { role: Role };
interface ButtonProps {
variant?: 'default' | 'destructive' | 'outline';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
Generics
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
return [value, setValue] as const;
}
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
Event Handlers
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSubmit(formData);
};
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
onClick?.();
};
Component Patterns
Basic Structure
interface CardProps {
title: string;
description?: string;
children: React.ReactNode;
className?: string;
}
export function Card({
title,
description,
children,
className
}: CardProps) {
return (
<div className={cn(
"rounded-lg border bg-card p-6 shadow-sm",
className
)}>
<h3 className="text-lg font-semibold">{title}</h3>
{description && (
<p className="mt-1 text-sm text-muted-foreground">
{description}
</p>
)}
<div className="mt-4">{children}</div>
</div>
);
}
Using shadcn/ui
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export function CreateItemDialog() {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Create Item</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Item</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit}>
<Input placeholder="Item name" />
<Button type="submit" className="mt-4">
Create
</Button>
</form>
</DialogContent>
</Dialog>
);
}
Tailwind Patterns
The cn() Utility
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Conditional Classes
<div
className={cn(
"rounded-lg border p-4 transition-colors",
isActive && "border-primary bg-primary/5",
isDisabled && "cursor-not-allowed opacity-50",
className
)}
>
{children}
</div>
Responsive Design
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{items.map((item) => (
<ItemCard key={item.id} item={item} />
))}
</div>
Common Patterns
<div className="flex min-h-screen items-center justify-center">
<div className="rounded-lg border bg-card p-6 shadow-sm">
// Flex with gap
<div className="flex items-center gap-2">
// Truncated text
<p className="truncate text-sm text-muted-foreground">
// Hover states
<button className="hover:bg-accent hover:text-accent-foreground">
Hooks
State Management
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<Item[]>([]);
type Action =
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_DATA'; payload: Data }
| { type: 'SET_ERROR'; payload: Error };
const [state, dispatch] = useReducer(reducer, initialState);
Effects
useEffect(() => {
const controller = new AbortController();
fetchData(controller.signal)
.then(setData)
.catch((error) => {
if (!controller.signal.aborted) {
setError(error);
}
});
return () => controller.abort();
}, [dependency]);
Custom Hooks
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue((v) => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse };
}
Data Fetching
Server Components (Next.js)
async function UserList() {
const users = await fetchUsers();
return (
<ul className="space-y-2">
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Client-Side Fetching
'use client';
function useUsers() {
const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const controller = new AbortController();
async function load() {
try {
const data = await fetchUsers(controller.signal);
setUsers(data);
} catch (e) {
if (!controller.signal.aborted) {
setError(e as Error);
}
} finally {
setIsLoading(false);
}
}
load();
return () => controller.abort();
}, []);
return { users, isLoading, error };
}
Forms
With React Hook Form
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const schema = z.object({
email: z.string().email("Invalid email"),
password: z.string().min(8, "Password must be at least 8 characters"),
});
type FormData = z.infer<typeof schema>;
export function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>({
resolver: zodResolver(schema),
});
const onSubmit = async (data: FormData) => {
await login(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<Input {...register("email")} placeholder="Email" />
{errors.email && (
<p className="mt-1 text-sm text-destructive">
{errors.email.message}
</p>
)}
</div>
<div>
<Input
{...register("password")}
type="password"
placeholder="Password"
/>
{errors.password && (
<p className="mt-1 text-sm text-destructive">
{errors.password.message}
</p>
)}
</div>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Signing in..." : "Sign In"}
</Button>
</form>
);
}
Naming Conventions
Files and Folders
- Components:
kebab-case.tsx or PascalCase.tsx
- Hooks:
use-hook-name.ts
- Utilities:
kebab-case.ts
- Types:
types.ts or type-name.ts
Code
function UserProfile() {}
function useAuth() {}
const API_BASE_URL = '/api';
const MAX_RETRY_COUNT = 3;
const handleClick = () => {};
const handleSubmit = () => {};
const handleInputChange = () => {};
const isLoading = true;
const hasError = false;
const canSubmit = true;
Error Handling
Error Boundaries
'use client';
interface ErrorBoundaryProps {
children: React.ReactNode;
fallback: React.ReactNode;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, { hasError: boolean }> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
API Error Handling
class ApiError extends Error {
constructor(
message: string,
public statusCode: number,
public code?: string
) {
super(message);
this.name = 'ApiError';
}
}
async function fetchWithError<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new ApiError(
'Request failed',
response.status,
response.statusText
);
}
return response.json();
}
Performance
Memoization
const sortedItems = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
const handleDelete = useCallback((id: string) => {
setItems((prev) => prev.filter((item) => item.id !== id));
}, []);
const MemoizedRow = memo(function Row({ item }: { item: Item }) {
return <div>{item.name}</div>;
});
Code Splitting
const HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() {
return (
<Suspense fallback={<Skeleton className="h-64 w-full" />}>
<HeavyChart data={data} />
</Suspense>
);
}
Common Commands
pnpm create next-app@latest my-app --typescript --tailwind --eslint
pnpm dlx shadcn@latest init
pnpm dlx shadcn@latest add button input dialog
pnpm add zod react-hook-form @hookform/resolvers
pnpm dev
pnpm build
Common Pitfalls
- Missing keys - Always provide stable, unique keys in lists
- Stale closures - Include all dependencies in useEffect/useCallback
- Prop drilling - Use context or composition for deep props
- Over-memoization - Profile before optimizing
- Type assertions - Prefer type guards over
as casting
- Missing 'use client' - Add directive for client-only features
- Inline objects in JSX - Creates new references on every render