Topics in this subject
React 4 min read Updated 5 Aug 2026

Design Patterns

Custom hooks, compound components, render props, HOCs, and controlled vs uncontrolled components.

Overview#

Modern React composes behavior with hooks; older codebases used render props and HOCs to share logic before hooks existed. Know all of them — interviews probe the legacy ones and expect you to explain why hooks replaced them.

flowchart TD
  A["Share logic between components"] --> B["Custom hook (modern default)"]
  A --> C["Render props (legacy)"]
  A --> D["HOC (legacy)"]
  E["Share structure/UI composition"] --> F["Compound components"]

Custom hooks — the modern default 🟢#

Extract stateful logic into a use* function. It's just a function calling other hooks; each caller gets its own isolated state. Replaces both render props and HOCs for logic reuse without wrapper nesting.

function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = useCallback(() => setOn((v) => !v), []);
  return [on, toggle];
}

function Panel() {
  const [open, toggle] = useToggle();
  return <button onClick={toggle}>{open ? "Hide" : "Show"}</button>;
}

Compound components#

Components that share implicit state via Context and only make sense together (like <select>/<option>). The parent owns state; children read it through context. Gives a flexible, declarative API without prop-drilling.

import { createContext, useContext, useState } from "react";

const TabsContext = createContext(null);

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

function Tab({ value, children }) {
  const { active, setActive } = useContext(TabsContext);
  return (
    <button aria-selected={active === value} onClick={() => setActive(value)}>
      {children}
    </button>
  );
}

function TabPanel({ value, children }) {
  const { active } = useContext(TabsContext);
  return active === value ? <div role="tabpanel">{children}</div> : null;
}

Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

// Usage — clean, self-describing API:
<Tabs defaultValue="a">
  <Tabs.Tab value="a">First</Tabs.Tab>
  <Tabs.Tab value="b">Second</Tabs.Tab>
  <Tabs.Panel value="a">Panel A</Tabs.Panel>
  <Tabs.Panel value="b">Panel B</Tabs.Panel>
</Tabs>

🎯 Consumers arrange the pieces freely while shared state stays encapsulated — the key selling point over a monolithic <Tabs items={[...]}/> prop API.

Render props (legacy)#

Pass a function as a prop (often children) that receives state and returns UI. Solved logic reuse pre-hooks but causes "wrapper hell" / nested callbacks.

function Mouse({ children }) {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  return (
    <div onMouseMove={(e) => setPos({ x: e.clientX, y: e.clientY })}>
      {children(pos)}
    </div>
  );
}

<Mouse>{({ x, y }) => <p>{x}, {y}</p>}</Mouse>

HOCs (legacy)#

A higher-order component is a function Component -> Component that injects props. Still seen in libraries (connect, withRouter). Downsides: prop collisions, wrapper nesting, unclear data origin.

function withUser(Wrapped) {
  return function WithUser(props) {
    const user = useAuth();
    return <Wrapped {...props} user={user} />;
  };
}
const ProfileWithUser = withUser(Profile);

⚠️ Hooks superseded HOCs and render props for logic sharing — they avoid extra tree nesting and make data flow explicit. Prefer a custom hook.

Container / presentational (legacy)#

Split "smart" containers (fetch/state) from "dumb" presentational components (props → UI). Hooks made the strict split less necessary since any component can hold logic via hooks, but the separation of concerns idea still guides good structure.

Controlled vs uncontrolled components 🎯#

Controlled Uncontrolled
Source of truth React state The DOM
Read value From state Via ref
Set value value + onChange defaultValue, DOM
Use when Validation, formatting, dependent fields Simple/one-shot, file inputs, perf
// Controlled
const [name, setName] = useState("");
<input value={name} onChange={(e) => setName(e.target.value)} />;

// Uncontrolled
const ref = useRef();
<input defaultValue="hi" ref={ref} />;   // read ref.current.value on submit

⚠️ <input type="file"> is always uncontrolled (read-only value). Switching an input between controlled/uncontrolled at runtime warns — pick one.

Interview Q&A#

Q1. Why did custom hooks replace render props and HOCs? They share stateful logic without adding wrapper components, avoiding nesting/"wrapper hell", prop collisions, and unclear data flow. Logic is a plain function each caller instantiates independently.

Q2. What are compound components and why use them? Related components sharing implicit state via Context (e.g. Tabs/Tab/TabPanel). They give a flexible, declarative composition API and encapsulate shared state without prop-drilling.

Q3. Controlled vs uncontrolled inputs? Controlled inputs derive their value from React state (value+onChange) — best for validation/formatting. Uncontrolled inputs let the DOM hold the value, read via ref — simpler and needed for file inputs.

Q4. What is a HOC and a downside? A function taking a component and returning an enhanced one that injects props. Downsides: wrapper nesting, prop-name collisions, and obscured data origin — reasons hooks replaced them.

Q5. Is container/presentational still relevant? The strict pattern is legacy since hooks let any component hold logic, but its underlying principle — separating data/behavior from pure rendering — still improves testability and reuse.