Skip to article content

React Patterns That Scale

As React applications grow beyond a handful of components, architecture becomes critical. The patterns you choose determine whether adding the next feature takes an hour or a week. Here are the patterns that have proven themselves in large-scale production React codebases.

Custom Hooks: The Foundation

Custom hooks are the single most impactful pattern in modern React. They let you extract stateful logic into reusable, testable units without changing your component hierarchy.

function useLocalStorage(key, initialValue) {
  const [stored, setStored] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value) => {
    const valueToStore = value instanceof Function 
      ? value(stored) 
      : value;
    setStored(valueToStore);
    window.localStorage.setItem(key, JSON.stringify(valueToStore));
  };

  return [stored, setValue];
}

The hook above encapsulates localStorage synchronization. Components using it get persistent state without knowing anything about the storage mechanism. Testing is straightforward — mock localStorage and assert behavior.

Compound Components

Compound components share implicit state between a parent and its children, giving consumers flexible composition without prop drilling. Think of <select> and <option> — they communicate internally.

// Usage - clean, composable API
<Tabs defaultValue="overview">
  <Tabs.List>
    <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
    <Tabs.Trigger value="code">Code</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="overview">...</Tabs.Content>
  <Tabs.Content value="code">...</Tabs.Content>
</Tabs>

// Implementation uses Context
const TabsContext = createContext();

function Tabs({ children, defaultValue }) {
  const [active, setActive] = useState(defaultValue);
  return (
    <TabsContext.Provider value={{ active, setActive }}>
      {children}
    </TabsContext.Provider>
  );
}

State Machines for Complex UI

When a component has more than 3 boolean flags, it is time for a state machine. State machines make impossible states unrepresentable and transitions explicit.

const machine = {
  idle: { FETCH: 'loading' },
  loading: { 
    SUCCESS: 'success', 
    ERROR: 'error' 
  },
  success: { RESET: 'idle', FETCH: 'loading' },
  error: { RETRY: 'loading', RESET: 'idle' },
};

function useStateMachine(initial) {
  const [state, setState] = useState(initial);
  const send = (event) => {
    const next = machine[state]?.[event];
    if (next) setState(next);
  };
  return [state, send];
}

Render Props (Still Useful)

While hooks replaced many render prop use cases, render props remain valuable for cases where you need to share behavior while allowing the consumer to control rendering entirely:

<DataFetcher url="/api/users">
  {({ data, loading, error }) => {
    if (loading) return <Skeleton />;
    if (error) return <ErrorBanner error={error} />;
    return <UserTable users={data} />;
  }}
</DataFetcher>

Container/Presentation Split

Separating data-fetching logic (containers) from rendering logic (presentational components) keeps your component tree testable and maintainable. Presentational components receive data via props and are easy to test — no mocking needed.

Error Boundaries with Retry

Production apps need graceful failure handling. Error boundaries catch render errors, and adding retry logic turns crashes into recoverable moments:

class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  retry = () => this.setState({ hasError: false, error: null });

  render() {
    if (this.state.hasError) {
      return this.props.fallback?.({
        error: this.state.error,
        retry: this.retry,
      }) || <DefaultError retry={this.retry} />;
    }
    return this.props.children;
  }
}

Key Principles

  • Colocation: Keep state as close to where it is used as possible
  • Composition over configuration: Many small components composed together beat one mega-component with 20 props
  • Explicit over implicit: When in doubt, make data flow visible
  • Single responsibility: A hook or component that does one thing well is better than one that does three things adequately

The goal is not to apply every pattern everywhere. The goal is to recognize when a piece of code is becoming hard to change, and to reach for the right pattern that makes the change simple again.