React Production Engineering
by @1kalin
Complete methodology for building production-grade React applications with architecture decisions, component design, state management, performance optimizati...
clawhub install afrexai-react-productionπ About This Skill
name: afrexai-react-production description: Complete methodology for building production-grade React applications with architecture decisions, component design, state management, performance optimization, testing, and deployment.
React Production Engineering
Complete methodology for building production-grade React applications. Covers architecture decisions, component design, state management, performance optimization, testing, and deployment β not just API reference, but engineering methodology with decision frameworks, templates, and scoring systems.
Phase 1: Architecture Assessment
Quick Health Check (score /16)
any types in production code (+2)Architecture Brief
project:
name: ""
type: "" # spa | ssr | hybrid | static
framework: "" # next | remix | vite-spa | astro
scale: "" # small (<20 routes) | medium (20-100) | large (100+)
team_size: "" # solo | small (2-5) | medium (6-15) | large (15+)
current_state:
react_version: "" # 18 | 19
typescript: true
router: "" # react-router | next-app | tanstack-router
state_management: "" # useState | zustand | jotai | redux | tanstack-query
styling: "" # tailwind | css-modules | styled-components | vanilla-extract
testing: "" # vitest | jest | playwright | cypress
ci_cd: "" # github-actions | gitlab-ci | vercel
pain_points: []
goals: []
Framework Selection Decision Matrix
| Factor | Vite SPA | Next.js | Remix | Astro | |--------|----------|---------|-------|-------| | SEO needed | β | β Best | β Good | β Best | | Dashboard/app | β Best | β Good | β Good | β | | Content-heavy | β | β Good | β Good | β Best | | Team familiarity | β Simple | β οΈ Learning curve | β οΈ Web standards | β οΈ Islands | | Deployment | Anywhere | Vercel optimal | Anywhere | Anywhere | | Bundle size | You control | Framework overhead | Smaller | Minimal JS |
Decision rules: 1. Dashboard/internal tool with no SEO β Vite SPA 2. Marketing + app hybrid β Next.js 3. Content-first with some interactivity β Astro 4. Web-standards-first, nested layouts β Remix 5. Default for most SaaS products β Next.js
Phase 2: Project Structure & Conventions
Recommended Feature-Based Structure
src/
βββ app/ # Routes/pages (framework-specific)
βββ features/ # Feature modules (THE core pattern)
β βββ auth/
β β βββ components/ # Feature-specific components
β β βββ hooks/ # Feature-specific hooks
β β βββ api/ # API calls & types
β β βββ utils/ # Feature utilities
β β βββ types.ts # Feature types
β β βββ index.ts # Public API (barrel export)
β βββ dashboard/
β βββ settings/
βββ shared/ # Cross-feature shared code
β βββ components/ # Generic UI components
β β βββ ui/ # Primitives (Button, Input, Card)
β β βββ layout/ # Layout components
β βββ hooks/ # Generic hooks
β βββ lib/ # Utilities, constants
β βββ types/ # Global types
βββ providers/ # Context providers
βββ styles/ # Global styles
7 Structure Rules
1. Feature isolation β features/ never import from other features directly; use shared/ or events 2. Barrel exports β every feature has index.ts that defines its public API 3. Colocation β tests, stories, and styles live next to their component 4. Max file size β 300 lines. If bigger, split 5. Max component size β 50 lines of JSX. If bigger, extract 6. No circular deps β enforce with eslint-plugin-import 7. Types colocated β feature types in feature, shared types in shared/typesNaming Conventions
Components: PascalCase.tsx (UserProfile.tsx)
Hooks: useCamelCase.ts (useAuth.ts)
Utilities: camelCase.ts (formatCurrency.ts)
Types: PascalCase.ts (User.ts) or types.ts
Constants: SCREAMING_SNAKE.ts (API_ENDPOINTS.ts)
Test files: *.test.tsx (UserProfile.test.tsx)
Story files: *.stories.tsx (Button.stories.tsx)
Phase 3: Component Design Patterns
Component Anatomy Template
// 1. Imports (grouped: react β third-party β internal β types β styles)
import { useState, useCallback, memo } from 'react'
import { clsx } from 'clsx'
import { Button } from '@/shared/components/ui'
import type { User } from '../types'// 2. Types (exported for reuse)
export interface UserCardProps {
user: User
onEdit?: (id: string) => void
variant?: 'compact' | 'full'
className?: string
}
// 3. Component (named export, not default)
export const UserCard = memo(function UserCard({
user,
onEdit,
variant = 'full',
className,
}: UserCardProps) {
// 4. Hooks first
const [isExpanded, setIsExpanded] = useState(false)
// 5. Derived state (no useEffect for derived!)
const displayName = ${user.firstName} ${user.lastName}
// 6. Handlers (useCallback for passed-down refs)
const handleEdit = useCallback(() => {
onEdit?.(user.id)
}, [onEdit, user.id])
// 7. Early returns for edge cases
if (!user) return null
// 8. JSX (max 50 lines)
return (
{displayName}
{variant === 'full' && {user.bio}
}
{onEdit && }
)
})
Component Composition Patterns
1. Compound Components (for related UI groups)
// Usage: A ...
const TabsContext = createContext(null)export function Tabs({ children, defaultValue }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultValue)
return (
{children}
)
}
Tabs.List = TabsList
Tabs.Tab = TabsTab
Tabs.Panel = TabsPanel
2. Render Props (for flexible rendering logic)
export function DataList({ items, renderItem, renderEmpty }: DataListProps) {
if (items.length === 0) return renderEmpty?.() ??
return {items.map((item, i) => - {renderItem(item)}
)}
}
3. Higher-Order Components (for cross-cutting concerns β use sparingly)
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 neededPhase 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
// 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
// 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 statePhase 5: Hooks Engineering
Custom Hook Template
// 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-libraryPhase 6: TypeScript Integration
Strict Configuration
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"paths": {
"@/*": ["./src/*"]
}
}
}
Essential Type Patterns
// 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. Zeroany β 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 liesPhase 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
// 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+)
// 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 guessworkPhase 8: Error Handling & Resilience
Error Boundary Architecture
// 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
onError / error statesPhase 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
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.inferexport 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
// 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 testPhase 11: Accessibility (a11y)
10-Point Accessibility Checklist
1. Semantic HTML β not , not