) {
if (items.length === 0) return renderEmpty?.() ??
return {items.map((item, i) => - {renderItem(item)}
)}
}
```
**3. Higher-Order Components (for cross-cutting concerns — use sparingly)**
```tsx
export function withAuth(Component: ComponentType
) {
return function AuthenticatedComponent(props: P) {
const { user, isLoading } = useAuth()
if (isLoading) return
if (!user) return
return
}
}
```
### 10 Component Rules
1. **One component per file** — always
2. **Named exports** — never default exports (refactoring safety)
3. **Props interface** — always explicit, always exported
4. **No business logic in components** — extract to hooks
5. **No inline styles** — use Tailwind classes or CSS modules
6. **No string refs** — useRef only
7. **No index as key** — use stable identifiers
8. **Memo strategically** — not everywhere, only for expensive renders
9. **Children over props** — prefer composition over configuration
10. **Accessible by default** — semantic HTML, ARIA when needed
---
## Phase 4: State Management Decision Framework
### State Type Decision Tree
```
Is it server data (from API)?
├─ YES → TanStack Query (or SWR) — NEVER Redux/Zustand for server state
│
└─ NO → Is it shared across features?
├─ YES → Is it complex with many actions?
│ ├─ YES → Zustand (or Redux Toolkit if team knows it)
│ └─ NO → Jotai (atomic) or Zustand (simple store)
│
└─ NO → Is it shared within a feature?
├─ YES → Context + useReducer (or Zustand feature store)
└─ NO → useState / useReducer (component-local)
```
### State Management Comparison
| Tool | Best For | Bundle | Learning | Team Size |
|------|----------|--------|----------|-----------|
| useState | Component-local | 0 KB | None | Any |
| useReducer | Complex local state | 0 KB | Low | Any |
| Context | Feature-scoped, low-frequency | 0 KB | Low | Any |
| Zustand | Global client state | 1.1 KB | Low | Any |
| Jotai | Atomic derived state | 3.4 KB | Medium | Small-Med |
| TanStack Query | Server state | 12 KB | Medium | Any |
| Redux Toolkit | Complex global + middleware | 11 KB | High | Large |
### Server State with TanStack Query
```tsx
// api/users.ts — query key factory pattern
export const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (filters: Filters) => [...userKeys.lists(), filters] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: string) => [...userKeys.details(), id] as const,
}
// hooks/useUsers.ts
export function useUsers(filters: Filters) {
return useQuery({
queryKey: userKeys.list(filters),
queryFn: () => fetchUsers(filters),
staleTime: 5 * 60 * 1000, // 5 min
placeholderData: keepPreviousData,
})
}
export function useUpdateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: updateUser,
onMutate: async (newUser) => {
// Optimistic update
await queryClient.cancelQueries({ queryKey: userKeys.detail(newUser.id) })
const previous = queryClient.getQueryData(userKeys.detail(newUser.id))
queryClient.setQueryData(userKeys.detail(newUser.id), newUser)
return { previous }
},
onError: (err, newUser, context) => {
queryClient.setQueryData(userKeys.detail(newUser.id), context?.previous)
},
onSettled: (data, err, variables) => {
queryClient.invalidateQueries({ queryKey: userKeys.detail(variables.id) })
queryClient.invalidateQueries({ queryKey: userKeys.lists() })
},
})
}
```
### Client State with Zustand
```tsx
// stores/useUIStore.ts — thin, focused stores
interface UIStore {
sidebarOpen: boolean
theme: 'light' | 'dark' | 'system'
toggleSidebar: () => void
setTheme: (theme: UIStore['theme']) => void
}
export const useUIStore = create()(
persist(
(set) => ({
sidebarOpen: true,
theme: 'system',
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
setTheme: (theme) => set({ theme }),
}),
{ name: 'ui-preferences' }
)
)
// Usage: const theme = useUIStore((s) => s.theme) — always use selectors!
```
### 5 State Management Rules
1. **Server state ≠ client state** — never mix them in the same store
2. **Smallest scope possible** — useState > Context > Zustand > Redux
3. **No useEffect for derived state** — use useMemo or compute inline
4. **Selectors always** — `useStore(s => s.field)` not `useStore()`
5. **URL is state** — search params, filters, pagination → URL, not React state
---
## Phase 5: Hooks Engineering
### Custom Hook Template
```tsx
// hooks/useDebounce.ts
export function useDebounce(value: T, delayMs: number = 300): T {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delayMs)
return () => clearTimeout(timer)
}, [value, delayMs])
return debouncedValue
}
```
### Essential Custom Hooks Library
| Hook | Purpose | When to Use |
|------|---------|-------------|
| `useDebounce` | Debounce value changes | Search inputs, resize |
| `useMediaQuery` | Responsive breakpoints | Conditional rendering |
| `useLocalStorage` | Persistent local state | Preferences, drafts |
| `useIntersection` | Viewport detection | Lazy load, infinite scroll |
| `usePrevious` | Track previous value | Animations, comparisons |
| `useClickOutside` | Detect outside clicks | Dropdowns, modals |
| `useEventListener` | Safe event binding | Keyboard, scroll, resize |
| `useToggle` | Boolean state toggle | Modals, accordions |
### Hook Rules (beyond React's rules)
1. **One concern per hook** — `useUserSearch` not `useEverything`
2. **Return tuple or object** — tuple for 1-2 values, object for 3+
3. **Accept options object** — `useDebounce(value, { delay: 300 })` scales better
4. **Handle cleanup** — every subscription/timer needs cleanup in useEffect return
5. **No hooks in conditions** — extract conditional logic into the hook body
6. **Test hooks independently** — use `renderHook` from testing-library
---
## Phase 6: TypeScript Integration
### Strict Configuration
```json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"paths": {
"@/*": ["./src/*"]
}
}
}
```
### Essential Type Patterns
```tsx
// 1. Discriminated unions for state machines
type AsyncState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
// 2. Polymorphic components
type ButtonProps = {
as?: C
variant?: 'primary' | 'secondary'
} & ComponentPropsWithoutRef
export function Button({
as,
variant = 'primary',
...props
}: ButtonProps) {
const Component = as || 'button'
return
}
// 3. Branded types for IDs
type UserId = string & { __brand: 'UserId' }
type PostId = string & { __brand: 'PostId' }
// 4. Zod for runtime validation
const userSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
role: z.enum(['admin', 'user', 'viewer']),
})
type User = z.infer
```
### 5 TypeScript Rules
1. **Zero `any`** — use `unknown` and narrow, or generics
2. **Zod at boundaries** — validate all external data (API, forms, URL params)
3. **Discriminated unions over optional fields** — `{ status: 'success'; data: T }` not `{ data?: T; error?: Error }`
4. **Branded types for IDs** — prevent `userId` being passed where `postId` expected
5. **Satisfies over as** — `config satisfies Config` preserves inference; `as Config` lies
---
## Phase 7: Performance Optimization
### Performance Budget
| Metric | Target | Measurement |
|--------|--------|-------------|
| First Contentful Paint | < 1.8s | Lighthouse |
| Largest Contentful Paint | < 2.5s | Lighthouse |
| Interaction to Next Paint | < 200ms | Lighthouse |
| Cumulative Layout Shift | < 0.1 | Lighthouse |
| Bundle size (gzipped) | < 200 KB | webpack-bundle-analyzer |
| JS execution (main thread) | < 3s | Chrome DevTools |
### Optimization Priority Stack
| Priority | Technique | Impact | Effort |
|----------|-----------|--------|--------|
| P0 | Code splitting (route-based) | 🔴 High | Low |
| P0 | Image optimization (next/image, srcset) | 🔴 High | Low |
| P1 | Tree shaking (named imports) | 🟡 Medium | Low |
| P1 | Virtualization for long lists | 🟡 Medium | Medium |
| P1 | Debounce expensive operations | 🟡 Medium | Low |
| P2 | React.memo on expensive components | 🟢 Low-Med | Low |
| P2 | useMemo/useCallback for expensive calculations | 🟢 Low-Med | Low |
| P3 | Web Workers for heavy computation | 🟢 Low | High |
### Code Splitting Patterns
```tsx
// 1. Route-based (automatic with Next.js, manual with React Router)
const Dashboard = lazy(() => import('./features/dashboard'))
const Settings = lazy(() => import('./features/settings'))
// 2. Component-based (heavy components)
const Chart = lazy(() => import('./components/Chart'))
const MarkdownEditor = lazy(() =>
import('./components/MarkdownEditor').then(m => ({ default: m.MarkdownEditor }))
)
// 3. Library-based (heavy third-party)
const { PDFViewer } = await import('@react-pdf/renderer')
```
### React Compiler (React 19+)
```tsx
// With React Compiler enabled, manual memo/useMemo/useCallback become unnecessary
// The compiler auto-memoizes. Remove manual optimizations:
// ❌ const memoized = useMemo(() => expensiveCalc(data), [data])
// ✅ const memoized = expensiveCalc(data) // compiler handles it
// Enable in babel config:
// plugins: [['babel-plugin-react-compiler', {}]]
```
### Rendering Performance Rules
1. **Never create components inside components** — define at module level
2. **Never create objects/arrays in JSX** — `style={{ color: 'red' }}` rerenders always
3. **Children as props prevent rerender** — ``
4. **Key must be stable and unique** — not index, not `Math.random()`
5. **Avoid context value churn** — memoize provider value or split contexts
6. **Profile before optimizing** — React DevTools Profiler, not guesswork
---
## Phase 8: Error Handling & Resilience
### Error Boundary Architecture
```tsx
// Three levels of error boundaries:
// 1. App-level (catches everything, shows full-page error)
// 2. Feature-level (isolates feature failures)
// 3. Component-level (for risky widgets — charts, third-party)
// Modern error boundary with react-error-boundary
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
function FeatureErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
Something went wrong
{error.message}
)
}
// Usage:
queryClient.clear()}>
```
### Error Handling Checklist
- [ ] App-level error boundary wrapping entire app
- [ ] Feature-level boundaries for each major feature
- [ ] API errors handled in TanStack Query's `onError` / error states
- [ ] Form validation errors shown inline (not alerts)
- [ ] 404 page for unknown routes
- [ ] Offline detection and graceful degradation
- [ ] Error reporting to monitoring (Sentry, etc.)
- [ ] User-friendly error messages (no stack traces in production)
---
## Phase 9: Forms & Validation
### Form Library Decision
| Library | Best For | Bundle | Renders |
|---------|----------|--------|---------|
| React Hook Form | Most forms | 9 KB | Minimal (uncontrolled) |
| Formik | Simple forms | 13 KB | Every keystroke |
| TanStack Form | Type-safe complex | 5 KB | Controlled |
| Native | 1-2 field forms | 0 KB | You control |
**Default recommendation: React Hook Form + Zod**
### Form Pattern
```tsx
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Min 8 characters'),
role: z.enum(['admin', 'user']),
})
type FormData = z.infer
export function LoginForm({ onSubmit }: { onSubmit: (data: FormData) => void }) {
const form = useForm({
resolver: zodResolver(schema),
defaultValues: { email: '', password: '', role: 'user' },
})
return (
)
}
```
---
## Phase 10: Testing Strategy
### Test Pyramid for React
| Level | Tool | Coverage Target | What to Test |
|-------|------|-----------------|-------------|
| Unit | Vitest | 80% business logic | Hooks, utilities, reducers |
| Component | Testing Library | Key user flows | Rendering, interactions, a11y |
| Integration | Testing Library | Feature flows | Multi-component workflows |
| E2E | Playwright | Critical paths | Auth, checkout, core flows |
| Visual | Chromatic/Percy | UI components | Regression detection |
### Testing Patterns
```tsx
// Component test (Testing Library philosophy: test behavior, not implementation)
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
describe('UserCard', () => {
it('calls onEdit when edit button clicked', async () => {
const user = userEvent.setup()
const onEdit = vi.fn()
render()
await user.click(screen.getByRole('button', { name: /edit/i }))
expect(onEdit).toHaveBeenCalledWith(mockUser.id)
})
it('does not render edit button when onEdit not provided', () => {
render()
expect(screen.queryByRole('button', { name: /edit/i })).not.toBeInTheDocument()
})
})
```
### 7 Testing Rules
1. **Test behavior, not implementation** — never test state directly or useEffect
2. **Use accessible queries** — `getByRole` > `getByTestId` > `getByText`
3. **User events over fireEvent** — `userEvent.click` simulates real interaction
4. **One assertion per concept** — not one per test, but focused assertions
5. **Mock at boundaries** — API calls, not internal functions
6. **No snapshot tests** — they break on every change and test nothing meaningful
7. **Arrange-Act-Assert** — clear structure in every test
---
## Phase 11: Accessibility (a11y)
### 10-Point Accessibility Checklist
1. **Semantic HTML** — `