Skip to main content

Command Palette

Search for a command to run...

React Design Patterns Every Senior Frontend Developer Should Know

Updated
6 min readView as Markdown
B
Senior Software Architect with 30+ years of experience building enterprise systems using Java, Spring Boot, and cloud-native technologies.

React is one of the most popular frontend frameworks in the world.

Learning React isn't difficult.

Building a React application that remains clean, scalable, and maintainable after three years of continuous development is.

Many developers know Hooks, Context, and React Router. Fewer understand the architectural patterns that make large React applications easier to extend, test, and maintain.

The difference between a mid-level and a senior frontend developer is rarely about knowing more APIs.

It's about making better architectural decisions.

Here are the React design patterns every senior frontend developer should know—with practical examples.


1. Presentational vs. Container Components

One of the most fundamental React design patterns is separating UI from business logic.

❌ Before

function UserProfile() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch("/api/user")
      .then(res => res.json())
      .then(setUser);
  }, []);

  return <div>{user?.name}</div>;
}

The component is responsible for:

  • Fetching data

  • Managing state

  • Rendering UI

Too many responsibilities.


✅ Better

function UserProfileContainer() {
  const user = useUser();

  return <UserProfile user={user} />;
}
function UserProfile({ user }) {
  return <div>{user.name}</div>;
}

Now:

  • UI becomes reusable

  • Business logic is isolated

  • Testing becomes much easier


2. Custom Hooks

Whenever you copy the same useEffect() twice...

...it's probably time for a custom hook.

❌ Before

useEffect(() => {
   fetch(...)
}, []);

Repeated across five components.


✅ Better

function useUser() {
   const [user, setUser] = useState(null);

   useEffect(() => {
      fetch("/api/user")
         .then(res => res.json())
         .then(setUser);
   }, []);

   return user;
}

Now every page simply uses:

const user = useUser();

Custom hooks make business logic reusable while keeping components focused on presentation.


3. Composition Over Configuration

Many developers create components with dozens of props.

<Button
   primary
   rounded
   loading
   shadow
   icon
   fullWidth
/>

As requirements grow, the API becomes increasingly difficult to understand.

Instead, compose smaller building blocks.

<Card>
   <Card.Header />
   <Card.Body />
   <Card.Footer />
</Card>

Composition produces:

  • cleaner APIs

  • more flexibility

  • fewer breaking changes

  • easier maintenance


4. Compound Components

This pattern lets related components communicate while keeping the API intuitive.

Example

<Tabs>
   <Tabs.List>
      <Tabs.Trigger>Home</Tabs.Trigger>
      <Tabs.Trigger>Profile</Tabs.Trigger>
   </Tabs.List>

   <Tabs.Content>
      ...
   </Tabs.Content>
</Tabs>

Instead of:

<Tabs
   tabs={tabs}
   selected={selected}
   content={content}
   orientation="horizontal"
/>

Libraries like Radix UI and Reach UI use this approach extensively.


5. State Colocation

A common mistake is lifting every piece of state to the top-level component.

App

 └── Dashboard

      └── Widget

            └── SearchBox

Search text lives inside App.

Every keypress re-renders the entire tree.

Instead:

Keep state as close as possible to where it is actually used.

Benefits include:

  • fewer renders

  • simpler code

  • better performance

  • easier debugging


6. Context for Shared Dependencies

Many developers misuse Context as a replacement for global state management.

Context works well for:

  • Theme

  • Authentication

  • Current User

  • Locale

  • Feature Flags

Example:

<AuthProvider>
   <App />
</AuthProvider>
const { user } = useAuth();

Context is ideal for relatively stable, widely shared values—not frequently changing application data.


7. Feature-Based Folder Structure

Many projects begin with this structure:

components/
hooks/
services/
utils/
pages/

After several years:

  • hundreds of components

  • hundreds of hooks

  • difficult navigation

A feature-based approach scales better.

features/

   users/

      components/

      hooks/

      api/

      types/

   orders/

   dashboard/

Each feature owns everything it needs.

Large enterprise teams often organize code around business capabilities rather than technical layers.


8. Headless Components

Separate functionality from appearance.

Instead of coupling behavior with styling:

<Dropdown />

Use headless components.

<Menu>
   <Menu.Button />
   <Menu.Items />
</Menu>

Behavior:

  • keyboard support

  • accessibility

  • focus management

Presentation:

  • fully customizable

This approach powers libraries such as Radix UI, Headless UI, and React Aria.


9. Server State ≠ Client State

One of the biggest architectural mistakes is treating server data like local component state.

Server state requires:

  • caching

  • retries

  • background refresh

  • synchronization

  • optimistic updates

Client state handles things like:

  • modal visibility

  • selected tab

  • form inputs

  • temporary UI state

Example:

const { data, isLoading } = useQuery({
   queryKey: ["users"],
   queryFn: fetchUsers
});

Rather than manually managing loading, caching, and error handling, dedicated libraries simplify server-state management.


10. Error Boundaries

Production applications should fail gracefully.

Instead of crashing the entire page:

Dashboard

├── Analytics

├── Orders

└── Billing

Wrap each major section with an Error Boundary.

<ErrorBoundary>
   <Billing />
</ErrorBoundary>

If Billing fails, the rest of the dashboard continues working.

This improves resilience and user experience.


11. Render Props (Still Worth Understanding)

Hooks replaced many render prop use cases, but you'll still encounter the pattern in mature codebases.

Example:

<DataLoader
   render={(users) => (
      <UserList users={users} />
   )}
/>

Understanding render props helps when maintaining legacy applications and recognizing how React patterns have evolved.


12. Build Components That Can Evolve

The best React components are not the most clever.

They're the easiest to extend.

Ask yourself:

  • Can another team reuse this component?

  • Can I test it independently?

  • Can new features be added without rewriting it?

  • Does it have a clear responsibility?

If the answer is yes, you're probably designing at a senior level.


Final Thoughts

React isn't difficult.

Scaling React applications is.

As products grow, technical debt rarely comes from React itself. It comes from components that take on too many responsibilities, state that's managed in the wrong place, and architecture that wasn't designed to evolve.

Senior frontend developers think beyond individual components. They design systems that are modular, predictable, and easy for other engineers to understand months or even years later.

Mastering these design patterns won't just make your code cleaner—it will make your team more productive, your applications more resilient, and your architecture ready for long-term growth.

Because great React applications aren't built by writing more code.

They're built by writing the right code.

More from this blog

B

Bill LIao's Blog

137 posts

A technical blog on modern backend development, software architecture, and practical AI agent workflows