DEV Community

Cover image for Scalable React Component Patterns: From Atomic Design to Compound Components
Vishw Patel
Vishw Patel

Posted on

Scalable React Component Patterns: From Atomic Design to Compound Components

Scalable React Component Patterns: From Atomic Design to Compound Components

As your React app grows, the way you organize and build components becomes critical. Instead of just “breaking things into components,” advanced design patterns help you:

  • Promote reusability
  • Reduce prop drilling
  • Create highly customizable UIs
  • Avoid tech debt

In this article, we’ll explore key React component patterns that help build modular, maintainable, and scalable frontends.


Atomic Design: Structuring Components by Granularity

Atomic Design (coined by Brad Frost) is a methodology that breaks the UI into five stages:

  • Atoms – Basic building blocks (e.g., Button, Input)
  • Molecules – Groups of atoms (e.g., Input + Label)
  • Organisms – Full sections (e.g., Header, Card)
  • Templates – Page-level layouts
  • Pages – Complete pages with data

Benefits:

  • Enforces consistency
  • Enables design system reuse
  • Scales well with teams and folders

Compound Components: Components That Work Together

Compound components are a pattern where multiple components share implicit state using React context.

<Tabs>
  <Tabs.List>
    <Tabs.Trigger value="1">Tab 1</Tabs.Trigger>
    <Tabs.Trigger value="2">Tab 2</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="1">Panel 1</Tabs.Content>
  <Tabs.Content value="2">Panel 2</Tabs.Content>
</Tabs>
Enter fullscreen mode Exit fullscreen mode

Why it works:

  • Components are declarative and readable
  • State is managed internally in Tabs, shared through context

This is used by libraries like Radix UI and Headless UI.


Controlled vs Uncontrolled Components

Controlled:

<input value={value} onChange={e => setValue(e.target.value)} />
Enter fullscreen mode Exit fullscreen mode
  • State is owned by the parent
  • Easier to control programmatically

Uncontrolled:

<input defaultValue="hello" ref={ref} />
Enter fullscreen mode Exit fullscreen mode
  • DOM manages the state
  • Ideal for quick forms or integrating with non-React libs

Use controlled when you need validation, centralized state, or interactivity.


Render Props: Component Logic Sharing

Render props is a technique to share logic by passing a function as a child.

<MouseTracker>
  {({ x, y }) => <div>Mouse position: {x}, {y}</div>}
</MouseTracker>
Enter fullscreen mode Exit fullscreen mode

Under the hood:

function MouseTracker({ children }) {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  // track mouse...
  return children(pos);
}
Enter fullscreen mode Exit fullscreen mode

Before hooks, this was one of the main ways to reuse logic. Still useful for fine-grained control.


Headless Components: Logic Without Markup

Headless components (like useCombobox() from Downshift) expose logic but let you define the UI.

const { isOpen, getItemProps, getMenuProps } = useCombobox({...});
Enter fullscreen mode Exit fullscreen mode

Why it scales:

  • Keeps logic and markup separate
  • Highly composable
  • Works with any design system

Great for building framework-agnostic, accessible libraries.


Presentational + Container Components

This classic pattern separates logic from UI:

Container:

function UserContainer() {
  const user = useUser();
  return <UserProfile user={user} />;
}
Enter fullscreen mode Exit fullscreen mode

Presentational:

function UserProfile({ user }) {
  return <div>{user.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

This is great when using tools like Storybook, where presentational components can be tested in isolation.


Contextual Components and Portals

Use React’s context to avoid prop-drilling and manage shared state across compound UIs.

Portals let you render children outside the parent DOM node — ideal for modals, dropdowns, tooltips:

ReactDOM.createPortal(<Modal />, document.body);
Enter fullscreen mode Exit fullscreen mode

Combined with context, this enables decoupled, accessible UI patterns.


Custom Hooks + Refs + Imperative Handles

Custom hooks let you extract and reuse logic cleanly.

function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = () => setOn(o => !o);
  return { on, toggle };
}
Enter fullscreen mode Exit fullscreen mode

useImperativeHandle lets you expose internal component methods to parent refs — useful for forms, sliders, and interactive components.

useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus() }));
Enter fullscreen mode Exit fullscreen mode

When and Where to Use Each Pattern

Pattern Best For
Atomic Design Design systems, scalable folder structure
Compound Components Tabs, modals, dropdowns
Render Props Custom logic with full control over rendering
Headless Components Logic-heavy reusable components
Container/Presentational Separation of concerns
Context + Portals Shared state across UI layers

Final Thoughts

Mastering these patterns enables you to:

  • Build clean, reusable, consistent UI systems
  • Collaborate better with designers and teams
  • Write more maintainable, readable code

Modern React is more than JSX — it’s a design system toolkit.

Pick the right pattern for the job, and your components will scale like pros. 💪✨


Neon image

Serverless Postgres in 300ms (❗️)

10 free databases with autoscaling, scale-to-zero, and read replicas. Start building without infrastructure headaches. No credit card needed.

Try for Free →

Top comments (0)

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay