Back to skills
extension
Category: Development & EngineeringNo API key required

react-design-patterns

Use when choosing a React component pattern — custom hooks, control props, compound components, headless components, render props, container/presentational, or other architectural patterns. Includes 13 patterns with decision guide and 2025 popularity ranking.

personAuthor: jakexiaohubgithub

React Design Patterns

Overview

13 patterns ranked by 2025 popularity. Golden rule: start with a Custom Hook — upgrade to Compound Components only if structural sharing is needed.

Pattern Decision Guide

| Need | Pattern | |------|---------| | Reusable logic, no UI opinion | Custom Hook | | Controlled vs uncontrolled component behavior | Control Props | | Tightly coupled UI subcomponents (Tabs, Modal, Accordion) | Compound Components | | Logic without imposed styling | Headless Component | | Separate data fetching from rendering | Container / Presentational | | Global stable state (auth, theme) | Provider (Context) | | Reverse data flow (child → parent) | Render Props | | Full render control via children | Children as Function | | Flexible API with pre-built props | Props Getters | | Crash isolation | Error Boundary | | Render outside DOM parent (modals, tooltips) | Portal | | Design system hierarchy | Atomic Design |

1. Custom Hook ⭐⭐⭐⭐⭐

Extract stateful logic into a reusable function. Most popular pattern in 2025.

function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try { return JSON.parse(localStorage.getItem(key)) ?? initialValue; }
    catch { return initialValue; }
  });
  const set = useCallback((v) => {
    setValue(prev => {
      const next = v instanceof Function ? v(prev) : v;
      localStorage.setItem(key, JSON.stringify(next));
      return next;
    });
  }, [key]);
  return [value, set];
}

2. Control Props ⭐⭐⭐⭐

Component supports both controlled (parent owns state via props) and uncontrolled (manages own state) modes. Every form input and component library uses this.

// Controlled — parent manages state
<EditableTitle title={title} onChange={setTitle} />

// Uncontrolled — component manages own state, parent can reset via key
<EditableTitle key={userId} defaultTitle="Untitled" />

// Implementation supporting both modes
function EditableTitle({ title, defaultTitle = '', onChange }) {
  const [internal, setInternal] = useState(defaultTitle);
  const isControlled = title !== undefined;
  const value = isControlled ? title : internal;

  const handleChange = (e) => {
    if (!isControlled) setInternal(e.target.value);
    onChange?.(e.target.value);
  };

  return <input value={value} onChange={handleChange} />;
}

3. Compound Components ⭐⭐⭐⭐

Family of subcomponents sharing state via local context. Used by Radix UI, Headless UI, React Aria.

const TabsContext = createContext(null);
function Tabs({ children, defaultTab }) { /* owns [activeTab, setActiveTab], provides both via context */ }
Tabs.Tab = /* reads context, renders the trigger button */
Tabs.Panel = /* reads context, renders children when active */

Shape: the parent owns the state and provides it via local context; dot-namespaced subcomponents consume it; a useTabs() guard hook throws when a subcomponent renders outside <Tabs>. The full runnable implementation lives in the react-usecontext skill (single owner of this example) — load it when you build one.

4. Headless Component ⭐⭐⭐⭐

Hook provides logic only — zero HTML, zero CSS. You own the UI completely.

function useAccordion() {
  const [openIndex, setOpenIndex] = useState(null);
  const toggle = useCallback((i) => setOpenIndex(prev => prev === i ? null : i), []);
  const isOpen = useCallback((i) => openIndex === i, [openIndex]);
  return { toggle, isOpen };
}
// Two completely different UIs can use the same hook

5. Container / Presentational ⭐⭐⭐

Split into: Container (logic, fetch, navigation) and Presentational (receives data via props, renders only).

// Presentational — pure UI, easy to test
const UserCard = ({ user, onFollow }) => (
  <div><h2>{user.name}</h2><button onClick={onFollow}>Follow</button></div>
);
// Container — logic and data
const UserCardContainer = ({ userId }) => {
  const { user, follow } = useUser(userId);
  if (!user) return <Skeleton />;
  return <UserCard user={user} onFollow={follow} />;
};

6. Provider (Context) ⭐⭐⭐⭐

createContext + useContext for global/local state. See react-usecontext skill for full details.

7. Render Props ⭐⭐

Pass a function as prop — component calls it with data:

<WindowSize render={({ width }) => (
  width < 768 ? <MobileMenu /> : <DesktopMenu />
)} />

Useful for reverse data flow (child → parent). Mostly replaced by custom hooks in 2025.

8. Children as Function ⭐⭐

Variant of render props — function passed as children. Popularized by Formik, Downshift:

<Toggle>
  {({ on, toggle }) => (
    <button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>
  )}
</Toggle>

9. Props Getters ⭐⭐

Hook returns pre-built prop objects to spread on elements. Used by React Hook Form (register), react-table:

function useToggle() {
  const [on, setOn] = useState(false);
  const getTogglerProps = (overrides = {}) => ({
    onClick: () => setOn(prev => !prev),
    'aria-pressed': on,
    ...overrides,
  });
  return { on, getTogglerProps };
}
// Usage: <button {...getTogglerProps({ className: 'my-btn' })}>

10. Error Boundary ⭐⭐

Catches JS errors in the component tree, shows fallback UI. Requires class component or react-error-boundary library:

import { ErrorBoundary } from 'react-error-boundary';
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => { /* reset state */ }}>
  <Dashboard />
</ErrorBoundary>

Underused — most apps should have granular error boundaries per section.

11. Portal ⭐⭐⭐

Renders outside the DOM parent tree. Solves z-index, overflow: hidden, clipping:

import { createPortal } from 'react-dom';
function Modal({ isOpen, children }) {
  if (!isOpen) return null;
  return createPortal(
    <div className="modal-overlay">{children}</div>,
    document.body
  );
}

12. HOC (Higher-Order Component) ⭐ — Legacy

Function that takes a component, returns a new component. Replaced by custom hooks in modern code.

// Legacy
const ProtectedDashboard = withAuth(Dashboard);
// Modern equivalent
function ProtectedRoute({ children }) {
  const { user } = useAuth();
  if (!user) return <Navigate to="/login" />;
  return children;
}

13. Atomic Design ⭐⭐⭐

Hierarchy: Atoms (Button) → Molecules (SearchBox = Input + Button) → Organisms (Header) → Templates (layout without data) → Pages (template with data). Great for design systems and large team projects.

References