Loading...
Loading...
Compound components, custom hooks, error boundaries — patterns SDE-2 interviews expect.
Module 13 · React Design Patterns
Compound components, controlled vs uncontrolled, render props, custom hooks, HOCs, provider pattern, error boundaries, portals, and state machines.
React design patterns for reusable, maintainable components: compound components, controlled vs uncontrolled, render props, custom hooks, container vs presentational, HOCs, provider pattern, error boundaries, Suspense, memoization, portals, and state machines.
React design patterns are a staple SDE-2 topic — interviews expect you to explain when to use compound components vs prop drilling, controlled vs uncontrolled inputs, and why custom hooks replaced HOCs and render props.
This module covers all 10 core patterns plus advanced techniques: Suspense, memoization, portals, and state machines — with code examples and a when-to-use guide.
Patterns are tools, not rules — you will learn which pattern solves prop drilling, repeated logic, cross-cutting auth, modal rendering, and complex multi-step flows without over-engineering simple components.
All 10 core patterns plus Suspense, memoization, portals, and XState state machines — each with code examples, a when-to-use table, and 30 interview questions from compound components to error boundaries.
Design pattern questions appear in every mid-to-senior React interview. You must explain compound components vs prop hell, why hooks replaced HOCs, how to prevent Context re-renders, and when to reach for state machines — not just name patterns. The best answers tie each pattern to a specific problem it solves.
Established practices for reusable, maintainable, scalable components.
React Design Patterns are established practices and solutions for building reusable, maintainable, and scalable React components. They solve common problems and improve code quality.
Compose UI naturally instead of stuffing 20 props into one component.
// ❌ BAD — Too many props <Tabs activeTab="home" onTabChange={handleTabChange} tabs={["home", "profile"]} orientation="horizontal" size="medium" /> // ✅ GOOD — Compound Components <Tabs> <Tabs.List> <Tabs.Trigger value="home">Home</Tabs.Trigger> <Tabs.Trigger value="profile">Profile</Tabs.Trigger> </Tabs.List> <Tabs.Content value="home">Home</Tabs.Content> <Tabs.Content value="profile">Profile</Tabs.Content> </Tabs>
const TabsContext = createContext(null); function Tabs({ children }) { const [activeTab, setActiveTab] = useState("home"); return ( <TabsContext.Provider value={{ activeTab, setActiveTab }}> {children} </TabsContext.Provider> ); } function TabsTrigger({ value, children }) { const { activeTab, setActiveTab } = useContext(TabsContext); return ( <button className={activeTab === value ? "active" : ""} onClick={() => setActiveTab(value)} > {children} </button> ); } Tabs.List = TabsList; Tabs.Trigger = TabsTrigger; Tabs.Content = TabsContent;
React state vs DOM state for form inputs.
| Aspect | Controlled | Uncontrolled |
|---|---|---|
| State | React state | DOM state |
| Predictable | Yes | No |
| Performance | Slower (re-renders) | Faster |
| Use Case | Validation, real-time | Simple inputs, file uploads |
// Controlled — value tied to React state function ControlledInput() { const [value, setValue] = useState(""); return ( <input value={value} onChange={(e) => setValue(e.target.value)} /> ); } // Uncontrolled — DOM manages state via ref function UncontrolledInput() { const inputRef = useRef(null); const handleSubmit = () => console.log(inputRef.current.value); return <input defaultValue="Hello" ref={inputRef} />; }
Rule of thumb: Controlled = predictable. Uncontrolled = simpler and faster. Many production apps mix both (semi-controlled).
Share logic without forcing a specific UI.
Abstract logic but let the consumer decide the UI. Useful when multiple components need the same behavior with different rendering.
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 <MouseTracker render={({ x, y }) => <p>Mouse position: {x}, {y}</p>} />
Modern alternative: Custom Hooks — prefer hooks over render props.
Extract business logic into reusable hooks.
function useAuth() { const [user, setUser] = useState(null); const login = (creds) => setUser(creds.user); const logout = () => setUser(null); return { user, login, logout }; } function LoginForm() { const { user, login } = useAuth(); const handleSubmit = (e) => { login({ email: e.email, password: e.password }); }; return <form onSubmit={handleSubmit}>{/* ... */}</form>; }
Separate logic from UI rendering.
// Container — logic + data function UserListContainer() { const [users, setUsers] = useState([]); useEffect(() => { fetch("/api/users").then((res) => setUsers(res.data)); }, []); return <UserList users={users} />; } // Presentational — UI only function UserList({ users }) { return ( <ul> {users.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }
Modern approach: use Custom Hooks instead of container components.
Components that define structure and wrap children.
function Layout({ children }) { return ( <div className="layout"> <header className="header">Header</header> <main className="main">{children}</main> <footer className="footer">Footer</footer> </div> ); } // Usage <Layout> <Dashboard /> </Layout>
Display different interfaces based on state or props.
// 1. If-else if (isLoading) return <Loading />; return <Content />; // 2. Ternary return isLoading ? <Loading /> : <Content />; // 3. Logical AND return showContent && <Content />; // 4. Switch switch (status) { case "loading": return <Loading />; case "success": return <Success />; case "error": return <Error />; default: return null; }
Wrap a component to inject additional logic.
function withAuth(WrappedComponent) { return function AuthComponent(props) { const { user } = useAuth(); if (!user) return <LoginPage />; return <WrappedComponent {...props} user={user} />; }; } const Dashboard = withAuth(function Dashboard({ user }) { return <div>Welcome {user.name}</div>; });
Modern alternative: Custom Hooks — HOCs add wrapper nesting and are harder to type in TypeScript.
Share data across components without prop drilling.
const ThemeContext = createContext(null); function ThemeProvider({ children }) { const [theme, setTheme] = useState("light"); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ); } function ThemedButton() { const { theme } = useContext(ThemeContext); return <button className={theme}>Click</button>; }
Context selectors avoid re-renders — only subscribe to the slice you need:
const name = useContextSelector(UserContext, (v) => v.name);
Catch render errors in child components.
Hooks cannot catch render errors — only class-based error boundaries can. Always wrap production apps, especially API-heavy sections.
class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } render() { if (this.state.hasError) { return <h2>Something went wrong</h2>; } return this.props.children; } } <ErrorBoundary> <Dashboard /> </ErrorBoundary>
Suspense, memoization, portals, and state machines.
<Suspense fallback={<Loading />}> <Profile /> </Suspense>
const result = useMemo(() => heavyCalc(data), [data]); const handleClick = useCallback(() => doSomething(val), [val]);
Performance boost — but don't overdo it.
function Modal({ children }) { return ReactDOM.createPortal( <div className="modal">{children}</div>, document.getElementById("modal-root") ); }
Render modals and tooltips outside nested DOM — avoids CSS z-index hacks.
const toggleMachine = createMachine({ initial: "off", states: { off: { on: { TOGGLE: "on" } }, on: { on: { TOGGLE: "off" } }, }, }); function Toggle() { const [state, send] = useMachine(toggleMachine); return ( <button onClick={() => send("TOGGLE")}> {state.matches("on") ? "ON" : "OFF"} </button> ); }
Perfect for forms, multi-step flows, and state explosion.
Match the pattern to the problem.
| Problem | Pattern |
|---|---|
| Too many props | Compound Components |
| Repeated logic | Custom Hooks |
| Painful re-renders | Context Selectors + Memoization |
| Complex flows | State Machines |
| Errors lurking | Error Boundaries |
| Modal/CSS issues | Portals |
| Share logic without UI | Render Props |
| Avoid prop drilling | Provider Pattern |
| Loading states | Suspense |
What SDE-2 interviewers expect you to know.
Common interview questions about React design patterns.