DNA⚛ïļ ReactAdvanced React Patterns
ðŸĢHatchlingReactPatternsHOCCustom HooksComposition

Advanced React Patterns

HOCs, render props, custom hooks, compound components — the patterns that make your code reusable and your architecture scalable.

Advanced React Patterns

As your React app grows, you'll find yourself writing the same logic over and over — authentication checks, data fetching, scroll tracking, form validation. Patterns are reusable solutions that keep your code DRY (Don't Repeat Yourself) and your architecture clean.

This chapter covers the major patterns, when to use each one, and how they've evolved over time.


Higher-Order Components (HOCs)

A Higher-Order Component is a function that takes a component and returns a new, enhanced component. It's a wrapper that adds behavior without modifying the original.

Think of it like a phone case. The phone (your component) stays the same, but the case (HOC) adds protection (authentication), style (theming), or features (analytics tracking).

The Classic Example: withAuth

function withAuth(WrappedComponent) {
  return function AuthenticatedComponent(props) {
    const { user, loading } = useAuth();
 
    if (loading) return <p>Loading...</p>;
    if (!user) return <p>Please log in to view this page.</p>;
 
    return <WrappedComponent {...props} user={user} />;
  };
}
 
// Usage
function Dashboard({ user }) {
  return <h1>Welcome back, {user.name}!</h1>;
}
 
const ProtectedDashboard = withAuth(Dashboard);
 
// In your app
<ProtectedDashboard />

Now ProtectedDashboard automatically handles authentication. You didn't change Dashboard at all — you just wrapped it.

Another Example: withLogger

function withLogger(WrappedComponent) {
  return function LoggedComponent(props) {
    useEffect(() => {
      console.log(`${WrappedComponent.name} mounted`);
      return () => console.log(`${WrappedComponent.name} unmounted`);
    }, []);
 
    return <WrappedComponent {...props} />;
  };
}
 
const LoggedDashboard = withLogger(Dashboard);

HOC Drawbacks

  • Wrapper hell — stacking multiple HOCs makes debugging painful: withAuth(withLogger(withTheme(Dashboard)))
  • Prop collisions — if two HOCs inject a prop with the same name, one overwrites the other
  • Hard to trace — React DevTools shows a nested tree of anonymous wrappers

Common Mistake: Using HOCs when a custom hook would be simpler. HOCs were the go-to pattern before hooks existed. Today, reach for a custom hook first and only use HOCs when you need to wrap the rendered JSX (like adding a loading screen or redirect).


Render Props

A render prop is a component that takes a function as a prop and calls it to determine what to render. Instead of the component deciding what the UI looks like, you decide — the component just provides the data or behavior.

Think of it like a food delivery service. The service (component) gets the food (data) ready, but you decide how to plate and serve it (what to render).

function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });
 
  useEffect(() => {
    const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);
 
  return render(position);
}
 
// Usage — you decide what to render with the mouse position
function App() {
  return (
    <div>
      <MouseTracker
        render={({ x, y }) => (
          <p>The mouse is at ({x}, {y})</p>
        )}
      />
 
      <MouseTracker
        render={({ x, y }) => (
          <div
            style={{
              position: 'fixed',
              left: x + 10,
              top: y + 10,
              background: 'yellow',
              padding: '4px 8px',
            }}
          >
            Tooltip follows your cursor!
          </div>
        )}
      />
    </div>
  );
}

Same MouseTracker behavior, completely different UIs. That's the power of render props — separation of behavior from presentation.

The "Children as a Function" Variant

Instead of a named prop, you can use children:

function DataFetcher({ url, children }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
 
  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(data => { setData(data); setLoading(false); });
  }, [url]);
 
  return children({ data, loading });
}
 
// Usage
<DataFetcher url="/api/users">
  {({ data, loading }) =>
    loading ? <p>Loading...</p> : <UserList users={data} />
  }
</DataFetcher>

Remember: Render props are powerful but can make your JSX harder to read with deeply nested callbacks. In most cases today, a custom hook is a cleaner alternative.


Custom Hooks — The Modern Solution

Custom hooks are the modern replacement for both HOCs and render props. They let you extract and share stateful logic as a plain function.

Think of it like creating your own recipe card. Instead of explaining the same cooking steps every time, you write them down once and just say "follow the recipe" whenever you need it.

From Render Prop to Custom Hook

That MouseTracker render prop? Here's the custom hook version:

function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
 
  useEffect(() => {
    const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);
 
  return position;
}
 
// Usage — clean and simple
function CursorTooltip() {
  const { x, y } = useMousePosition();
 
  return (
    <div style={{ position: 'fixed', left: x + 10, top: y + 10 }}>
      Tooltip!
    </div>
  );
}
 
function CursorCoordinates() {
  const { x, y } = useMousePosition();
  return <p>Mouse: ({x}, {y})</p>;
}

No wrappers, no render callbacks, no extra nesting. Just call the hook and use the data.

Real-World Example: useInfiniteScroll

Let's build a practical custom hook that detects when the user scrolls near the bottom of a page:

function useInfiniteScroll(callback, options = {}) {
  const { threshold = 200 } = options;
  const [isFetching, setIsFetching] = useState(false);
 
  useEffect(() => {
    function handleScroll() {
      const scrolledToBottom =
        window.innerHeight + window.scrollY >= document.body.offsetHeight - threshold;
 
      if (scrolledToBottom && !isFetching) {
        setIsFetching(true);
      }
    }
 
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, [isFetching, threshold]);
 
  useEffect(() => {
    if (!isFetching) return;
 
    callback().then(() => {
      setIsFetching(false);
    });
  }, [isFetching, callback]);
 
  return { isFetching };
}
 
// Usage
function UserFeed() {
  const [users, setUsers] = useState([]);
  const [page, setPage] = useState(1);
 
  const loadMore = useCallback(async () => {
    const newUsers = await fetch(`/api/users?page=${page}`).then(r => r.json());
    setUsers(prev => [...prev, ...newUsers]);
    setPage(prev => prev + 1);
  }, [page]);
 
  const { isFetching } = useInfiniteScroll(loadMore);
 
  return (
    <div>
      {users.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
      {isFetching && <p>Loading more users...</p>}
    </div>
  );
}

Rules for Custom Hooks

  1. Name starts with use — this tells React (and the linter) it's a hook
  2. Can call other hooks — useState, useEffect, other custom hooks
  3. Each call gets its own state — two components using useMousePosition each have independent state

Think of it like: Custom hooks are like USB adapters. They package up complex wiring (state, effects, refs) into a simple plug. Any component can plug in and get the behavior without knowing the internals.


Compound Components

Compound components are a group of components that work together and share implicit state. The parent manages the state, and the children tap into it via context.

Think of it like a TV and remote control. They're separate pieces, but they work together through a shared connection (infrared signal = context). You can arrange the remote's buttons however you want, but they all control the same TV.

const TabsContext = React.createContext(null);
 
function Tabs({ children, defaultTab }) {
  const [activeTab, setActiveTab] = useState(defaultTab);
 
  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}
 
function TabList({ children }) {
  return <div className="tab-list" role="tablist">{children}</div>;
}
 
function Tab({ value, children }) {
  const { activeTab, setActiveTab } = useContext(TabsContext);
  const isActive = activeTab === value;
 
  return (
    <button
      role="tab"
      className={isActive ? 'tab active' : 'tab'}
      onClick={() => setActiveTab(value)}
    >
      {children}
    </button>
  );
}
 
function TabPanel({ value, children }) {
  const { activeTab } = useContext(TabsContext);
  if (activeTab !== value) return null;
  return <div role="tabpanel">{children}</div>;
}
 
Tabs.TabList = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

Usage — the consumer controls the layout and composition:

function SettingsPage() {
  return (
    <Tabs defaultTab="profile">
      <Tabs.TabList>
        <Tabs.Tab value="profile">Profile</Tabs.Tab>
        <Tabs.Tab value="security">Security</Tabs.Tab>
        <Tabs.Tab value="notifications">Notifications</Tabs.Tab>
      </Tabs.TabList>
 
      <Tabs.Panel value="profile">
        <ProfileSettings />
      </Tabs.Panel>
      <Tabs.Panel value="security">
        <SecuritySettings />
      </Tabs.Panel>
      <Tabs.Panel value="notifications">
        <NotificationSettings />
      </Tabs.Panel>
    </Tabs>
  );
}

The consumer decides how many tabs, what order, what goes in each panel. The Tabs family handles all the state management behind the scenes.

When to use compound components:

  • Component libraries (Select, Tabs, Accordion, Menu)
  • When users need flexible composition with shared state
  • When props drilling would create a messy API

Composition Over Inheritance

React has no class inheritance patterns. Instead, everything is built through composition — combining simple pieces to create complex UIs.

Think of it like LEGO blocks. You don't create a new type of brick by inheriting from an existing brick. You combine different bricks to build whatever you want.

The children Pattern

The simplest form of composition:

function Card({ children }) {
  return <div className="card">{children}</div>;
}
 
function InfoCard({ title, children }) {
  return (
    <Card>
      <h2>{title}</h2>
      <div>{children}</div>
    </Card>
  );
}
 
// Usage
<InfoCard title="Welcome">
  <p>This is a composed component built from simple pieces.</p>
</InfoCard>

The Slot Pattern

For layouts with multiple "holes" to fill:

function PageLayout({ header, sidebar, children }) {
  return (
    <div className="layout">
      <header>{header}</header>
      <aside>{sidebar}</aside>
      <main>{children}</main>
    </div>
  );
}
 
// Usage — each slot is filled independently
<PageLayout
  header={<NavBar />}
  sidebar={<SideMenu />}
>
  <ArticleContent />
</PageLayout>

Specialization

Creating specific components from general ones:

function Button({ variant, size, children, ...props }) {
  return (
    <button className={`btn btn-${variant} btn-${size}`} {...props}>
      {children}
    </button>
  );
}
 
function DangerButton(props) {
  return <Button variant="danger" {...props} />;
}
 
function SmallButton(props) {
  return <Button size="small" {...props} />;
}

DangerButton isn't a subclass of Button. It's a composition — a Button with a pre-filled prop.

Remember: If you ever think "I need to extend this component," stop and think about composition instead. Can you wrap it? Can you pass it as children? Can you pre-fill some props? Almost always, the answer is yes.


Pattern Selection Guide

Here's when to reach for each pattern:

PatternUse WhenExample
Custom HookYou need to share stateful logic between componentsuseAuth, useInfiniteScroll, useLocalStorage
Compound ComponentsYou need flexible composition with shared stateTabs, Select, Accordion, Menu
Composition (children/slots)You need layout flexibility without shared stateCard, PageLayout, Modal
HOCYou need to wrap many components with the same behavior that includes JSXwithAuth (shows login screen), withErrorBoundary
Render PropsYou need inversion of control — the consumer decides what to renderRare today — usually a custom hook is better

The Decision Flowchart

  1. Do you need to share logic (no JSX)? Use a custom hook
  2. Do you need shared state with flexible child composition? Use compound components
  3. Do you need to wrap many components with conditional rendering? Use an HOC
  4. Do you need the consumer to control what renders with shared data? Use render props
  5. Do you just need flexible layout? Use composition (children/slots)

Interview Q&A:

Q: When would you choose a custom hook over a Higher-Order Component?

Almost always. Custom hooks are simpler, don't create extra wrapper elements in the component tree, don't have prop collision problems, and are easier to compose (you can call multiple hooks in one component). The only time an HOC might still be useful is when you need to wrap the component's rendered output — like showing a login screen instead of the component, or wrapping it in an error boundary. But even those cases can often be handled with composition.

Q: What's the main benefit of compound components over a single component with many props?

Flexibility. A <Tabs items={[...]} /> component locks consumers into a fixed structure. Compound components like <Tabs>, <Tab>, <TabPanel> let consumers control the order, layout, and content of each piece while still sharing state. It's the difference between a pre-built desk and LEGO blocks.


Chapter Summary

PatternKey IdeaAnalogy
HOCWrap a component with extra behaviorPhone case adds features to the phone
Render PropsLet the consumer decide what to renderFood delivery — you plate the food yourself
Custom HookExtract and share stateful logicRecipe card — write it once, reuse everywhere
Compound ComponentsComponents that work together via shared contextTV and remote — separate pieces, shared connection
CompositionCombine simple pieces into complex UIsLEGO blocks — build anything from basic pieces

The evolution of React patterns tells a clear story:

  • 2015-2018: HOCs and render props were the main tools
  • 2019+: Custom hooks replaced most HOC and render prop use cases
  • Today: Custom hooks for logic, compound components for flexible UI, composition for everything else