# Frontend Manager Reference Guide
**Comprehensive reference for frontend development patterns, best practices, and common pitfalls.**
---
## Core Principles
1. **Component-First**: Build reusable, composable components
2. **Accessibility-First**: WCAG 2.1 AA as minimum standard
3. **Performance-First**: Core Web Vitals as targets
4. **Type-Safety**: TypeScript for all components
5. **Test-Driven**: Components should be testable
---
## React Patterns
### Component Structure
```typescript
// components/Button/Button.tsx
import { forwardRef, type ButtonHTMLAttributes } from 'react';
import { cn } from '@/lib/utils';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = 'primary', size = 'md', loading, children, disabled, ...props }, ref) => {
return (
<button
ref={ref}
className={cn(
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
'disabled:pointer-events-none disabled:opacity-50',
{
'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
'bg-gray-100 text-gray-900 hover:bg-gray-200': variant === 'secondary',
'hover:bg-gray-100': variant === 'ghost',
'h-8 px-3 text-sm': size === 'sm',
'h-10 px-4': size === 'md',
'h-12 px-6 text-lg': size === 'lg',
},
className
)}
disabled={disabled || loading}
{...props}
>
{loading && <Spinner className="mr-2 h-4 w-4" />}
{children}
</button>
);
}
);
Button.displayName = 'Button';
```
### Hook Patterns
```typescript
// hooks/useAsync.ts
import { useState, useCallback, useEffect } from 'react';
interface UseAsyncState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
interface UseAsyncResult<T> extends UseAsyncState<T> {
execute: () => Promise<void>;
reset: () => void;
}
export function useAsync<T>(
asyncFunction: () => Promise<T>,
immediate = true
): UseAsyncResult<T> {
const [state, setState] = useState<UseAsyncState<T>>({
data: null,
loading: immediate,
error: null,
});
const execute = useCallback(async () => {
setState({ data: null, loading: true, error: null });
try {
const data = await asyncFunction();
setState({ data, loading: false, error: null });
} catch (error) {
setState({ data: null, loading: false, error: error as Error });
}
}, [asyncFunction]);
const reset = useCallback(() => {
setState({ data: null, loading: false, error: null });
}, []);
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { ...state, execute, reset };
}
```
### Context Pattern
```typescript
// context/AuthContext.tsx
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
interface User {
id: string;
email: string;
name: string;
}
interface AuthContextValue {
user: User | null;
loading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
}
const AuthContext = createContext<AuthContextValue | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const login = useCallback(async (email: string, password: string) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) throw new Error('Login failed');
const { user } = await response.json();
setUser(user);
}, []);
const logout = useCallback(async () => {
await fetch('/api/auth/logout', { method: 'POST' });
setUser(null);
}, []);
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
```
---
## State Management
### When to Use What
| Approach | Use For | Examples |
|----------|---------|----------|
| `useState` | Local UI state | Form inputs, toggles |
| `useReducer` | Complex local state | Multi-field forms |
| Context | Theme, auth, i18n | Shared across tree |
| Zustand/Redux | Global app state | User data, cart |
| React Query | Server state | API data with caching |
### Zustand Store
```typescript
// stores/cartStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartStore {
items: CartItem[];
addItem: (item: Omit<CartItem, 'quantity'>) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
total: () => number;
}
export const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
addItem: (item) => set((state) => {
const existing = state.items.find((i) => i.id === item.id);
if (existing) {
return {
items: state.items.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
),
};
}
return { items: [...state.items, { ...item, quantity: 1 }] };
}),
removeItem: (id) => set((state) => ({
items: state.items.filter((i) => i.id !== id),
})),
updateQuantity: (id, quantity) => set((state) => ({
items: state.items.map((i) =>
i.id === id ? { ...i, quantity } : i
),
})),
clearCart: () => set({ items: [] }),
total: () => get().items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
),
}),
{ name: 'cart-storage' }
)
);
```
### React Query
```typescript
// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
interface User {
id: string;
name: string;
email: string;
}
async function fetchUsers(): Promise<User[]> {
const res = await fetch('/api/users');
if (!res.ok) throw new Error('Failed to fetch users');
return res.json();
}
async function createUser(data: Omit<User, 'id'>): Promise<User> {
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error('Failed to create user');
return res.json();
}
export function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}
```
---
## Form Handling
### React Hook Form + Zod
```typescript
// components/LoginForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const loginSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Minimum 8 characters'),
});
type LoginFormData = z.infer<typeof loginSchema>;
interface LoginFormProps {
onSubmit: (data: LoginFormData) => Promise<void>;
}
export function LoginForm({ onSubmit }: LoginFormProps) {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
setError,
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
});
const handleFormSubmit = async (data: LoginFormData) => {
try {
await onSubmit(data);
} catch (error) {
setError('root', { message: 'Invalid credentials' });
}
};
return (
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
<div className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email
</label>
<input
id="email"
type="email"
{...register('email')}
className="mt-1 block w-full rounded-md border p-2"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
<p id="email-error" className="mt-1 text-sm text-red-600" role="alert">
{errors.email.message}
</p>
)}
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium">
Password
</label>
<input
id="password"
type="password"
{...register('password')}
className="mt-1 block w-full rounded-md border p-2"
aria-invalid={!!errors.password}
/>
{errors.password && (
<p className="mt-1 text-sm text-red-600" role="alert">
{errors.password.message}
</p>
)}
</div>
{errors.root && (
<p className="text-sm text-red-600" role="alert">
{errors.root.message}
</p>
)}
<button
type="submit"
disabled={isSubmitting}
className="w-full rounded-md bg-blue-600 py-2 text-white disabled:opacity-50"
>
{isSubmitting ? 'Logging in...' : 'Log In'}
</button>
</div>
</form>
);
}
```
---
## Accessibility Checklist
### ARIA Requirements
```tsx
// Modal example
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<h2 id="modal-title">Confirm Action</h2>
<p id="modal-description">Are you sure you want to proceed?</p>
</div>
// Button states
<button
aria-pressed={isActive}
aria-disabled={isLoading}
aria-busy={isLoading}
>
{isLoading ? 'Loading...' : 'Submit'}
</button>
// Form errors
<input
aria-invalid={!!error}
aria-describedby={error ? 'error-message' : undefined}
/>
<p id="error-message" role="alert">{error}</p>
// Live regions for dynamic content
<div aria-live="polite" aria-atomic="true">
{notification}
</div>
```
### Keyboard Navigation
```typescript
// Keyboard handler for custom components
function handleKeyDown(event: React.KeyboardEvent) {
switch (event.key) {
case 'Enter':
case ' ':
event.preventDefault();
handleClick();
break;
case 'Escape':
handleClose();
break;
case 'ArrowDown':
event.preventDefault();
focusNext();
break;
case 'ArrowUp':
event.preventDefault();
focusPrevious();
break;
}
}
```
### Focus Management
```typescript
// Focus trap for modals
import { useRef, useEffect } from 'react';
function useFocusTrap(isActive: boolean) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!isActive || !containerRef.current) return;
const container = containerRef.current;
const focusableElements = container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
firstElement?.focus();
function handleKeyDown(e: KeyboardEvent) {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement?.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement?.focus();
}
}
container.addEventListener('keydown', handleKeyDown);
return () => container.removeEventListener('keydown', handleKeyDown);
}, [isActive]);
return containerRef;
}
```
---
## Performance Patterns
### Code Splitting
```typescript
// Route-based splitting
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
// Component-based splitting
const HeavyEditor = lazy(() => import('./components/HeavyEditor'));
// Preload on hover
const Analytics = lazy(() => import('./pages/Analytics'));
const preloadAnalytics = () => import('./pages/Analytics');
<Link to="/analytics" onMouseEnter={preloadAnalytics}>
Analytics
</Link>
```
### Memoization Guidelines
```typescript
// DO memoize:
// - Expensive calculations
const sortedItems = useMemo(() =>
items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// - Callbacks passed to memoized children
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
// - Components with expensive rendering
const ExpensiveChart = memo(function Chart({ data }) {
return <canvas>{/* complex rendering */}</canvas>;
});
// DON'T memoize:
// - Simple calculations
// - Callbacks for native elements
// - Components that re-render anyway
```
---
## Testing Patterns
### Component Testing
```typescript
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('submits with valid data', async () => {
const user = userEvent.setup();
const onSubmit = jest.fn();
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
await user.click(screen.getByRole('button', { name: /log in/i }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
});
it('shows validation errors', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={jest.fn()} />);
await user.click(screen.getByRole('button', { name: /log in/i }));
expect(await screen.findByText(/invalid email/i)).toBeInTheDocument();
});
});
```
---
## Common Pitfalls
| Pitfall | Symptom | Solution |
|---------|---------|----------|
| Stale closures | Wrong values in callbacks | Add deps to useCallback |
| Missing keys | React warnings, bugs | Use unique, stable keys |
| Prop drilling | Deeply nested props | Use context or state lib |
| Hydration mismatch | SSR warnings | Use useEffect for client |
| Memory leaks | Performance issues | Cleanup in useEffect |
| Over-rendering | Slow UI | Profile and memoize |
---
## Quick Reference
```typescript
// Utility function for className merging
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Common hooks
const [state, setState] = useState(initial);
const ref = useRef<HTMLElement>(null);
useEffect(() => { /* effect */ return () => { /* cleanup */ }; }, [deps]);
const memoized = useMemo(() => expensive(), [deps]);
const callback = useCallback(() => {}, [deps]);
const context = useContext(MyContext);
```