DEV Community

Mohsen Fallahnejad
Mohsen Fallahnejad

Posted on

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>
}
Enter fullscreen mode Exit fullscreen mode

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></>
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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 })
Enter fullscreen mode Exit fullscreen mode
  • 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 }
}
Enter fullscreen mode Exit fullscreen mode

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 },
})
Enter fullscreen mode Exit fullscreen mode

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>)
}
Enter fullscreen mode Exit fullscreen mode

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 })
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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',
})
Enter fullscreen mode Exit fullscreen mode

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.

Originally published on: Bitlyst

Top comments (0)