Frontend Design Patterns — A Practical Guide
This guide focuses on practical patterns you can apply today in React/Next.js (but most concepts are framework‑agnostic).
Core Principles
- Composition over inheritance: build small parts and combine them.
- Single responsibility: each component does one thing well.
- Lift state up only when multiple children need it.
- Prefer pure/controlled components for predictability.
- Co-locate logic, styles, tests near the component.
Component Patterns
1) Presentational vs Container (Smart/Dumb)
- Presentational: UI only, gets data via props.
- Container: handles data fetching/state, passes props down.
// Container
function UserCardContainer() {
const { data } = useQuery({ queryKey: ['user'], queryFn: fetchUser })
return <UserCard user={data} />
}
// Presentational
function UserCard({ user }) {
return <div className="card">Hello, {user.name}</div>
}
2) Controlled vs Uncontrolled
- Controlled: value lives in React state.
- Uncontrolled: DOM keeps the value; you read refs when needed.
// Controlled
function NameInputControlled() {
const [name, setName] = useState('')
return <input value={name} onChange={e => setName(e.target.value)} />
}
// Uncontrolled
function NameInputUncontrolled() {
const ref = useRef(null)
const submit = () => console.log(ref.current?.value)
return <><input ref={ref} /><button onClick={submit}>Save</button></>
}
3) Compound Components
Let parent own state; children communicate via context.
const TabsContext = createContext(null)
export function Tabs({ children }) {
const [active, setActive] = useState(0)
return <TabsContext.Provider value={{active, setActive}}>{children}</TabsContext.Provider>
}
Tabs.List = function List({ children }) { return <div role="tablist">{children}</div> }
Tabs.Tab = function Tab({ index, children }) {
const ctx = useContext(TabsContext)
return <button role="tab" aria-selected={ctx.active===index} onClick={() => ctx.setActive(index)}>{children}</button>
}
Tabs.Panel = function Panel({ index, children }) {
const ctx = useContext(TabsContext)
return ctx.active===index ? <div role="tabpanel">{children}</div> : null
}
4) Render Props / HOCs / Custom Hooks
- Prefer custom hooks in modern React for logic reuse.
// Custom hook (logic)
function useCountdown(ms: number) {
const [left, setLeft] = useState(ms)
useEffect(() => { const id = setInterval(() => setLeft(v => Math.max(0, v-1000)), 1000); return () => clearInterval(id) }, [])
return left
}
// Usage
function OfferTimer() {
const left = useCountdown(10_000)
return <p>Time left: {Math.ceil(left/1000)}s</p>
}
State Management Patterns
- Local UI state:
useState/useReducer
(forms, toggles). - Derived state: compute from source state; avoid duplicating.
- Global state: only for cross‑cutting data (auth, theme). Use Context, Zustand, Redux Toolkit, or Jotai.
- Server state: use React Query/SWR (caching, refetching, dedup).
// Server state example with React Query
const { data, isLoading, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
- State machines: XState/Zustand FSM for complex flows (explicit states, transitions).
Data Fetching & Boundaries
- Suspense for async UI boundaries (loading fallbacks).
- Error boundaries for uncaught render errors.
- Skeletons for perceived performance.
- Pagination/Infinite loading to avoid large payloads.
// Error boundary (simplified)
class ErrorBoundary extends React.Component {
state = { hasError: false }
static getDerivedStateFromError() { return { hasError: true } }
render() { return this.state.hasError ? <p>Something went wrong.</p> : this.props.children }
}
Styling Patterns
- Utility‑first: Tailwind for consistency + speed.
- CSS Modules: predictable scoping.
- CSS‑in‑JS (MUI emotion/styled-components): dynamic styles, theming.
- Design tokens: one source of truth for colors/spacing/typography.
// MUI theme tokens
const theme = createTheme({
palette: { primary: { main: '#0d6efd' } },
shape: { borderRadius: 12 },
})
Layout & Composition
- Page → Section → Component hierarchy.
- Slot pattern for flexible composition.
function Card({ header, children, footer }) {
return (<div className="card">
<div className="card__header">{header}</div>
<div className="card__body">{children}</div>
<div className="card__footer">{footer}</div>
</div>)
}
Performance Patterns
- Memoization:
useMemo
,useCallback
,React.memo
for expensive work/props stability. - Virtualization:
react-window
for big lists. - Code splitting: dynamic imports to reduce initial JS.
- Avoid re‑renders: key props, stable handlers, co-locate state.
const Chart = dynamic(() => import('./Chart'), { ssr: false })
Architecture Patterns
- Atomic Design (Atoms/Molecules/Organisms/Templates/Pages).
- Feature‑Sliced Design: by domain (entities/features/shared).
- Monorepo packages (PNPM/Turbo): share UI + utils.
- Micro‑frontends (Module Federation): for very large orgs (use sparingly).
Testing Patterns
- Unit: pure functions/components (Jest/Vitest).
- Integration: components with real children (React Testing Library).
- E2E: user flows (Playwright/Cypress). Prefer testing behavior, not implementation details.
// RTL example
render(<Button onClick={fn} />)
await user.click(screen.getByRole('button'))
expect(fn).toHaveBeenCalled()
Accessibility & UX
- Use semantic HTML and ARIA roles.
- Manage focus (modals, menus).
- Ensure keyboard and screen reader support.
- Color contrast, prefers‑reduced‑motion, i18n/rtl.
Error Handling & Resilience
- Graceful fallbacks, retry with backoff, offline/optimistic UI.
- Centralized toast/alert system.
toast.promise(api.save(data), {
loading: 'Saving…',
success: 'Saved!',
error: 'Failed to save',
})
Checklist (TL;DR)
- Separate UI from data/side‑effects.
- Reuse logic via custom hooks.
- Choose the right state scope (local/global/server).
- Add boundaries (loading/error).
- Style with tokens + consistent system.
- Watch re‑renders, split code, virtualize lists.
- Test pyramid: unit → integration → E2E.
- Ship accessible, resilient UX.
How did you like this post?
👍0
❤️0
🔥0
🤔0
😮0