Instruction file imported from EminjanO/react-typescript-labs (
.cursor/rules/react.mdc). Copyright stays with the author.
description: React best practices and patterns for this codebase globs: .tsx,.jsx alwaysApply: false
React Development Guidelines
Core Principles
- Function Components Only: No class components - use hooks for state and lifecycle
- TypeScript First: Every component, prop, and hook must be properly typed
- Composition Over Inheritance: Build complex UIs from simple, composable pieces
- Performance by Default: Consider performance implications in initial implementation
- Accessibility Always: Every interactive element must be keyboard and screen reader accessible
Component Architecture
File Structure
src/
├── components/
│ ├── ui/ # Reusable UI components (Button, Input, Card)
│ ├── layout/ # Layout components (Header, Footer, Sidebar)
│ └── features/ # Feature-specific components
├── hooks/ # Custom React hooks
├── utils/ # Utility functions
├── types/ # Shared TypeScript types
└── assets/ # Images, fonts, etc.
Component Organization
// 1. Imports (in order)
import { useState, useEffect, type FC } from 'react';
import { z } from 'zod';
import clsx from 'clsx';
// 2. Type definitions
interface ComponentProps {
// Props interface
}
// 3. Schema definitions (if needed)
const PropsSchema = z.object({
// Validation schema
});
// 4. Component definition
export const Component: FC<ComponentProps> = (props) => {
// 5. Hooks
const [state, setState] = useState();
// 6. Event handlers
const handleClick = () => {};
// 7. Effects
useEffect(() => {}, []);
// 8. Render
return <div />;
};
// 9. Display name (for debugging)
Component.displayName = 'Component';
State Management Patterns
Local State
// Simple state for UI-only concerns
const [isOpen, setIsOpen] = useState(false);
// Complex state with reducer for business logic
const [state, dispatch] = useReducer(reducer, initialState);
Lifted State
// Lift state to lowest common ancestor
export function Parent() {
const [sharedState, setSharedState] = useState();
return (
<>
<ChildA state={sharedState} />
<ChildB onUpdate={setSharedState} />
</>
);
}
Global State (Context)
// Create context with proper typing
const StateContext = createContext<StateValue | undefined>(undefined);
// Provider with value memoization
export const StateProvider: FC<{ children: ReactNode }> = ({ children }) => {
const [state, setState] = useState();
const value = useMemo(
() => ({ state, setState }),
[state]
);
return <StateContext.Provider value={value}>{children}</StateContext.Provider>;
};
// Custom hook with error boundary
export const useAppState = () => {
const context = useContext(StateContext);
if (!context) {
throw new Error('useAppState must be used within StateProvider');
}
return context;
};
Performance Optimization
Memoization Rules
// Memo for expensive components
export const ExpensiveComponent = memo(({ data }: Props) => {
return <ComplexVisualization data={data} />;
});
// useCallback for stable function references
const handleSubmit = useCallback((data: FormData) => {
// Process data
}, [dependency]);
// useMemo for expensive calculations
const processedData = useMemo(() => {
return expensiveCalculation(rawData);
}, [rawData]);
Code Splitting
// Route-based splitting
const Dashboard = lazy(() => import('@/pages/Dashboard'));
// Component-based splitting for heavy components
const HeavyChart = lazy(() => import('@/components/HeavyChart'));
// With loading boundary
<Suspense fallback={<Spinner />}>
<HeavyChart data={data} />
</Suspense>
List Optimization
// Always use stable, unique keys
items.map((item) => <Item key={item.id} {...item} />)
// Virtualize long lists (100+ items)
import { FixedSizeList } from 'react-window';
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
>
{({ index, style }) => (
<div style={style}>
<Item {...items[index]} />
</div>
)}
</FixedSizeList>
Styling with TailwindCSS
Class Name Organization
// Use clsx for conditional classes
import clsx from 'clsx';
<div
className={clsx(
// Base styles first
'rounded-lg border p-4',
// Conditional styles
{
'border-blue-500 bg-blue-50': isActive,
'border-gray-300 bg-white': !isActive,
},
// Size variants
{
'text-sm': size === 'small',
'text-base': size === 'medium',
'text-lg': size === 'large',
},
// State styles
'hover:shadow-md focus:outline-none focus:ring-2',
// Override with className prop
className
)}
/>
Component Variants with CVA
import { cva, type VariantProps } from 'class-variance-authority';
const buttonVariants = cva(
// Base styles
'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
ghost: 'hover:bg-gray-100',
},
size: {
sm: 'h-9 px-3 text-sm',
md: 'h-10 px-4',
lg: 'h-11 px-8',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);
interface ButtonProps
extends HTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
loading?: boolean;
}
export const Button: FC<ButtonProps> = ({
variant,
size,
className,
loading,
disabled,
children,
...props
}) => {
return (
<button
className={clsx(buttonVariants({ variant, size }), className)}
disabled={disabled || loading}
{...props}
>
{loading ? <Spinner /> : children}
</button>
);
};
Form Handling
Controlled Components
export function ControlledForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
});
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
// Process formData
};
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={formData.name}
onChange={handleChange}
/>
<input
name="email"
value={formData.email}
onChange={handleChange}
/>
<button type="submit">Submit</button>
</form>
);
}
React Hook Form + Zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const FormSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
age: z.number().min(18, 'Must be at least 18'),
});
type FormData = z.infer<typeof FormSchema>;
export function ValidatedForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>({
resolver: zodResolver(FormSchema),
defaultValues: {
name: '',
email: '',
age: 18,
},
});
const onSubmit = async (data: FormData) => {
// Data is validated and typed
await api.submitForm(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input {...register('name')} placeholder="Name" />
{errors.name && (
<span className="text-red-500">{errors.name.message}</span>
)}
</div>
<div>
<input {...register('email')} type="email" placeholder="Email" />
{errors.email && (
<span className="text-red-500">{errors.email.message}</span>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
Custom Hooks Patterns
Data Fetching Hook
export function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const abortController = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url, {
signal: abortController.signal,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
setError(err);
}
} finally {
setLoading(false);
}
};
fetchData();
return () => abortController.abort();
}, [url]);
return { data, loading, error };
}
Local Storage Hook
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((val: T) => T)) => void] {
// Get from local storage then parse stored json or return initialValue
const readValue = useCallback((): T => {
if (typeof window === 'undefined') {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
}, [initialValue, key]);
const [storedValue, setStoredValue] = useState<T>(readValue);
const setValue = useCallback(
(value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error);
}
},
[key, storedValue]
);
useEffect(() => {
setStoredValue(readValue());
}, [readValue]);
return [storedValue, setValue];
}
Testing Best Practices
Component Testing
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { vi } from 'vitest';
import { Button } from './Button';
describe('Button', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});
it('handles click events', async () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
it('shows loading state', () => {
render(<Button loading>Submit</Button>);
expect(screen.getByRole('button')).toBeDisabled();
expect(screen.queryByText('Submit')).not.toBeInTheDocument();
});
});
Hook Testing
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('increments counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});
Error Boundaries
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<
{ children: ReactNode; fallback?: ReactNode },
ErrorBoundaryState
> {
constructor(props: { children: ReactNode; fallback?: ReactNode }) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
this.props.fallback || (
<div className="p-4 text-red-600">
<h2>Something went wrong.</h2>
<details>
<summary>Error details</summary>
<pre>{this.state.error?.toString()}</pre>
</details>
</div>
)
);
}
return this.props.children;
}
}
// Usage
<ErrorBoundary fallback={<ErrorFallback />}>
<App />
</ErrorBoundary>
Accessibility Guidelines
Semantic HTML
// ✅ Good
<button onClick={handleClick}>Click me</button>
<nav>{/* navigation items */}</nav>
<main>{/* main content */}</main>
// ❌ Bad
<div onClick={handleClick}>Click me</div>
<div className="navigation">{/* navigation items */}</div>
ARIA Attributes
// Labeling
<button aria-label="Close dialog">×</button>
// Live regions
<div aria-live="polite" aria-atomic="true">
{notification && <p>{notification}</p>}
</div>
// Descriptions
<input
aria-describedby="email-error"
aria-invalid={!!errors.email}
/>
<span id="email-error">{errors.email?.message}</span>
Focus Management
export function Modal({ isOpen, onClose, children }: ModalProps) {
const closeButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen) {
closeButtonRef.current?.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div role="dialog" aria-modal="true">
<button ref={closeButtonRef} onClick={onClose} aria-label="Close">
×
</button>
{children}
</div>
);
}
Common Patterns
Render Props
interface RenderPropProps<T> {
data: T[];
renderItem: (item: T, index: number) => ReactNode;
}
function List<T>({ data, renderItem }: RenderPropProps<T>) {
return (
<ul>
{data.map((item, index) => (
<li key={index}>{renderItem(item, index)}</li>
))}
</ul>
);
}
Compound Components
interface TabsContextValue {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabsContext = createContext<TabsContextValue | undefined>(undefined);
export function Tabs({ children, defaultTab }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
Tabs.List = function TabsList({ children }: { children: ReactNode }) {
return <div className="tab-list">{children}</div>;
};
Tabs.Tab = function Tab({ value, children }: TabProps) {
const context = useContext(TabsContext);
if (!context) throw new Error('Tab must be used within Tabs');
return (
<button
className={clsx('tab', { active: context.activeTab === value })}
onClick={() => context.setActiveTab(value)}
>
{children}
</button>
);
};
Tabs.Panel = function TabPanel({ value, children }: TabPanelProps) {
const context = useContext(TabsContext);
if (!context) throw new Error('TabPanel must be used within Tabs');
if (context.activeTab !== value) return null;
return <div className="tab-panel">{children}</div>;
};
// Usage
<Tabs defaultTab="tab1">
<Tabs.List>
<Tabs.Tab value="tab1">Tab 1</Tabs.Tab>
<Tabs.Tab value="tab2">Tab 2</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="tab1">Content 1</Tabs.Panel>
<Tabs.Panel value="tab2">Content 2</Tabs.Panel>
</Tabs>
Anti-Patterns to Avoid
❌ Direct DOM Manipulation
// Bad
document.getElementById('myDiv').style.display = 'none';
// Good
const [isVisible, setIsVisible] = useState(true);
return isVisible && <div id="myDiv" />;
❌ Using Array Indexes as Keys
// Bad
items.map((item, index) => <Item key={index} />)
// Good
items.map((item) => <Item key={item.id} />)
❌ Inline Function Creation in Render
// Bad
<button onClick={() => handleClick(item.id)}>Click</button>
// Good
const handleItemClick = useCallback((id: string) => {
// handle click
}, []);
<button onClick={() => handleItemClick(item.id)}>Click</button>
❌ Mutating State Directly
// Bad
state.items.push(newItem);
setState(state);
// Good
setState(prev => ({
...prev,
items: [...prev.items, newItem]
}));
Debugging Tips
- Use React DevTools - Essential for component inspection
- Add displayName - Makes debugging easier
- Use Error Boundaries - Catch and handle errors gracefully
- Console.log with labels -
console.log('Component render:', { props, state }) - Use debugger statement - Pause execution at specific points
- Check React.StrictMode warnings - Catch potential issues early
File Naming Conventions
- Components: PascalCase (
Button.tsx,UserCard.tsx) - Hooks: camelCase with 'use' prefix (
useAuth.ts,useLocalStorage.ts) - Utilities: camelCase (
formatDate.ts,validateEmail.ts) - Types: PascalCase with suffix (
UserTypes.ts,ApiTypes.ts) - Constants: SCREAMING_SNAKE_CASE in files (
API_URL,MAX_RETRIES)
Summary
Follow these guidelines to build maintainable, performant, and accessible React applications. Remember:
- Type everything - No implicit any
- Validate external data - Use Zod for runtime validation
- Optimize thoughtfully - Measure before optimizing
- Test behavior - Not implementation details
- Keep it simple - Complexity is the enemy of maintainability