React

Design Patterns That Transformed My React Code

A practical guide to React patterns I use daily: Compound Components, Custom Hooks, State Machines, Context architecture, and more, with real-world examples.

3 min read
ReactDesign PatternsTypeScriptFrontendArchitecture
Design Patterns That Transformed My React Code

Beyond Basic React

Writing React components is easy. Writing React components that scale, that other developers understand instantly, and that don't become maintenance nightmares—that's an art. These patterns have transformed how I build applications.

The biggest shift for me was moving from thinking of components as UI boxes to thinking of them as APIs. Once a component is treated as an API surface, composition, predictability, and documentation become much more important than cleverness.

1. Compound Components

This pattern changed everything for me. Instead of passing dozens of props to configure a component, you compose it from smaller pieces. Think of how HTML's <select> and <option> work together.

The Power

  • Flexible composition without prop drilling
  • Clear, declarative APIs
  • Each piece is independently testable
  • Users control the layout

I use this pattern for Tabs, Accordions, Dropdown menus, and any component where users need control over structure.

Example: Tabs API

tsxCompound components example
1function Tabs({ children }: { children: React.ReactNode }) {
2  return <div className="tabs">{children}</div>;
3}
4
5Tabs.List = function List({ children }: { children: React.ReactNode }) {
6  return <div className="tabs-list">{children}</div>;
7};
8
9Tabs.Panel = function Panel({ children }: { children: React.ReactNode }) {
10  return <div className="tabs-panel">{children}</div>;
11};
12
13// Usage
14<Tabs>
15  <Tabs.List>
16    <button>Overview</button>
17    <button>Details</button>
18  </Tabs.List>
19  <Tabs.Panel>Content here</Tabs.Panel>
20</Tabs>;

2. Custom Hooks: Extract Everything

If you're not writing custom hooks, you're missing React's most powerful feature. Any stateful logic that could be reused should become a hook.

My Rules for Hooks

  • One responsibility per hook
  • Return tuples for simple state, objects for complex
  • Handle loading and error states consistently
  • Document the expected behavior

Hooks I Write for Every Project

  • useLocalStorage: Persistent state that syncs across tabs
  • useDebounce: Delay expensive operations
  • useMediaQuery: Responsive logic in JavaScript
  • useOnClickOutside: Close modals and dropdowns properly

Example: A simple reusable hook

tsxReusable debounce hook
1function useDebounce<T>(value: T, delay = 300) {
2  const [debouncedValue, setDebouncedValue] = React.useState(value);
3
4  React.useEffect(() => {
5    const timer = setTimeout(() => setDebouncedValue(value), delay);
6    return () => clearTimeout(timer);
7  }, [value, delay]);
8
9  return debouncedValue;
10}

3. State Machines for Complex UI

When your component has more than three states, reach for a state machine. They make impossible states impossible and complex flows understandable.

A login form isn't just "loading" or "not loading". It's idle, validating, submitting, succeeded, or failed. Each state has specific allowed transitions. State machines encode this explicitly.

tsReducer-driven state machine
1type AuthState =
2  | { status: "idle" }
3  | { status: "submitting" }
4  | { status: "success" }
5  | { status: "error"; message: string };
6
7function authReducer(state: AuthState, action: { type: string }) {
8  switch (action.type) {
9    case "submit":
10      return { status: "submitting" };
11    case "success":
12      return { status: "success" };
13    case "error":
14      return { status: "error", message: "Login failed" };
15    default:
16      return state;
17  }
18}

Even if you do not use a formal state machine library, the discipline of explicit states and transitions keeps your UI honest.

4. Render Props (Still Useful)

Hooks have replaced most render prop use cases, but the pattern still shines for components that need to share DOM-related state—like mouse position or scroll position.

The lesson here is not that older patterns are bad. The lesson is that the simplest abstraction that solves the problem is usually the right one.

5. Provider Pattern for Global State

Context API gets a bad rap for performance, but used correctly—with split providers and memoization—it's perfect for truly global state like themes, auth, and feature flags.

Example: Split read/write contexts

tsxSplit context provider
1const ThemeContext = React.createContext("light");
2const ThemeDispatchContext = React.createContext<(theme: string) => void>(() => {});
3
4function ThemeProvider({ children }: { children: React.ReactNode }) {
5  const [theme, setTheme] = React.useState("light");
6
7  return (
8    <ThemeContext.Provider value={theme}>
9      <ThemeDispatchContext.Provider value={setTheme}>
10        {children}
11      </ThemeDispatchContext.Provider>
12    </ThemeContext.Provider>
13  );
14}

Splitting context this way avoids unnecessary re-renders for consumers that only need one side of the state.

6. Composition Over Configuration

One of the most important React ideas is that composition usually beats giant prop lists. If a component is getting difficult to configure, consider exposing smaller building blocks instead of more options.

When to Reach for Each Pattern

  • Compound Components: when structure should be flexible
  • Custom Hooks: when logic should be reusable
  • State Machines: when state transitions get complicated
  • Context Providers: when state must be shared globally
  • Render Props: when you need a reusable stateful wrapper

The Meta-Pattern

The real pattern is knowing which pattern to use. Start simple. Add complexity only when the code demands it. The best pattern is the one that makes your specific problem disappear.

Senior React work is less about memorizing a long list of patterns and more about choosing the right trade-off for the current problem.

Continuous Learning

React patterns evolve. Hooks replaced class lifecycle methods. Server Components are changing how we think about data fetching. Stay curious, keep learning, and always question if there's a better way.

The best React code is not the most clever code. It is the code that is easy for the next developer to change without fear.

Prabhath Madhushan

Prabhath Madhushan

Full Stack Developer | Software Engineer

A passionate developer building scalable web applications with modern technologies. Always learning, always creating.