React

Render Props & Patterns

14 Questions

The render props pattern is a technique for sharing stateful logic between components by passing a function as a prop — the component provides the data, the consumer decides how to render it. Unlike HOCs, render props make the data flow explicit: you can see exactly what values are being passed at the call site. This pattern was widely used before custom hooks, and is still found in libraries like React Router and Formik. The function prop is usually named render or children, and receives state or behavior as its arguments.

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

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

Why it matters: The render props pattern lets you share dynamic behavior (like mouse position or data fetching) while leaving the rendering decision to the consumer.

Real applications: Mouse tracking, data fetching components, toggle state sharing — any behavior you want to share but render differently in each place.

Common mistakes: Defining the render prop function inline — creates a new function on every render, defeating React.memo optimization on the component.

When you use children as a function, the component calls props.children(data) instead of a named prop like render, resulting in cleaner JSX syntax at the call site. This is sometimes called the function-as-children pattern and is more ergonomic since you don't need to remember the prop name. The consuming component passes a function between the opening and closing tags, receiving the shared data as arguments. Formik and data-fetching wrappers use this pattern to expose values, errors, and isSubmitting.

<DataFetcher url="/api/users">
  {({ data, loading }) => loading ? <Spinner /> : <UserList users={data} />}
</DataFetcher>

Why it matters: Using children as the function is more ergonomic than a named render prop — there's no separate prop name to remember.

Real applications: Formik uses this to expose form state; data fetching wrappers pass loading/data/error to the child function.

Common mistakes: Forgetting that children is a function in this pattern — calling it without arguments or not calling it at all breaks rendering.

The compound component pattern lets a group of related components share implicit state through React Context without requiring the consumer to wire them up manually. The parent component owns the state and provides it via context; child components read that context independently to coordinate behavior. This results in an expressive, flexible API where consumers arrange child components freely while behavior still coordinates correctly. Libraries like Radix UI and Headless UI use compound components for their Tabs, Select, and Dialog implementations.

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

<Tabs>
  <TabList><Tab>One</Tab><Tab>Two</Tab></TabList>
  <TabPanels><Panel>Content 1</Panel><Panel>Content 2</Panel></TabPanels>
</Tabs>

Why it matters: Compound components share implicit state instead of passing props through every level — the parent manages state, children consume it via context.

Real applications: Tab components, accordion menus, dropdown selects — any UI where multiple child pieces need to coordinate.

Common mistakes: Passing state down as explicit props instead of using context, which defeats the purpose and tightly couples the children to the parent API.

The container/presentational pattern separates components into two roles: container components that manage data fetching, API calls, and state, and presentational components that receive data as props and purely render UI. This separation makes each component easier to test in isolation — presentational components are essentially pure functions of their props with no side effects. The pattern was popularized by Dan Abramov and was the standard approach before custom hooks. Today, custom hooks have largely replaced container components for logic sharing, but separating concerns remains a valuable principle.

Why it matters: It keeps components focused on one job — easier to test each piece independently.

Real applications: A UserListContainer fetches users and passes them to a UserList component that only handles display.

Common mistakes: Letting presentational components make API calls — this breaks the separation and makes them harder to reuse.

The provider pattern uses React Context to make shared data (like themes, auth state, or locale) available to any component in the tree without passing props through every intermediate layer. A Provider component wraps part of the tree and supplies values; deeply nested consumers access them via useContext. Pairing the provider with a custom hook (like useTheme) is best practice — it gives consumers a clean API and hides the Context implementation. Use this for truly global or cross-cutting concerns, not for every piece of state.

const ThemeContext = createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

const useTheme = () => useContext(ThemeContext);

Why it matters: The provider pattern gives deeply nested components access to shared data without prop drilling.

Real applications: Theme providers, auth providers, locale providers — any global data every component might need.

Common mistakes: Putting frequently changing values (like scroll position) in context — every consumer re-renders on each change.

In controlled components, React state is the single source of truth — every keystroke updates state via onChange, and state drives the value prop. In uncontrolled components, the DOM holds the value and you access it imperatively via a ref only when needed (e.g., on form submit). Controlled components enable real-time validation, conditional logic, and instant UI feedback as the user types. Uncontrolled components are simpler for basic forms or when integrating with non-React libraries, but offer less fine-grained control.

Why it matters: Controlled inputs let you validate, transform, or block input as the user types — uncontrolled inputs only give you the final value.

Real applications: Controlled inputs for real-time validation; uncontrolled inputs for simple file uploads or third-party form libraries.

Common mistakes: Mixing controlled and uncontrolled — setting a value prop but not an onChange handler causes a React warning and frozen input.

The observer pattern decouples data producers from data consumers — components subscribe to an external store and automatically re-render when the store pushes updates to them. This is the core mechanism behind MobX, Zustand, and Jotai. In React 18, useSyncExternalStore is the recommended API for subscribing to external stores safely in concurrent mode. The key principle is that components never poll for changes; the store notifies all registered subscribers when its data changes.

Why it matters: The observer pattern decouples data from UI — components don't need to know about each other, only about the shared store.

Real applications: MobX observables, Zustand subscriptions, Redux store listeners — all use this pattern under the hood.

Common mistakes: Subscribing to a store inside a render without cleaning up — causes memory leaks when the component unmounts.

Composition over inheritance means building complex UI by combining simple, focused components rather than extending a class hierarchy. React avoids component inheritance entirely — instead, you pass components as props, nest them as children, or share logic through hooks. This keeps each component independent and easy to reason about, making it straightforward to mix and match capabilities. The React docs explicitly state that no use case has been found where component inheritance solves a problem that composition cannot.

Why it matters: Composition is more flexible — you can mix and match pieces without being locked into an inheritance hierarchy.

Real applications: A Button that accepts an icon prop or children is more reusable than a class that extends BaseButton.

Common mistakes: Creating deep class hierarchies to share behavior — this makes code rigid and hard to change when requirements shift.

The state reducer pattern gives component consumers control over how internal state transitions work, by accepting a custom reducer function as a prop. The component calls the reducer with each action and uses the returned state, letting consumers override specific action types while default behavior handles everything else. This makes reusable hooks and library components highly extensible without exposing all internals. Kent C. Dodds popularized this pattern; Downshift uses it to let you prevent the dropdown from closing on item selection.

function useToggle({ reducer = (state, action) => action } = {}) {
  const [on, dispatch] = useReducer(reducer, false);
  const toggle = () => dispatch({ type: 'toggle' });
  return { on, toggle };
}

Why it matters: The state reducer pattern lets callers override internal state transitions — giving power users full control without breaking the default behavior.

Real applications: Downshift uses this so you can prevent the dropdown from closing on selection; any library hook that needs to be customizable.

Common mistakes: Not providing a default reducer — always fall back to the built-in logic so basic usage works without any configuration.

Layout components define shared page structure — navigation, sidebar, header, footer — and inject the page-specific content through children or named props. This ensures every page in your app has a consistent visual frame without duplicating the structure in every route component. Modern frameworks like Next.js have built this pattern into their routing system with layout.tsx files that automatically wrap all child routes. The layout component should be completely generic — it renders the structural shell and never contains page-specific logic.

function DashboardLayout({ children }) {
  return (
    <div className="dashboard">
      <Sidebar />
      <main>{children}</main>
      <Footer />
    </div>
  );
}

Why it matters: Layout components separate page structure from page content — you define the shell once and reuse it across many pages.

Real applications: Dashboard layouts with a sidebar and header; admin panels where every page shares the same navigation frame.

Common mistakes: Hardcoding page-specific content inside the layout component — keep the layout generic and inject content through children or named slots.

The slot pattern uses named props to inject JSX content into specific, distinct regions of a component — similar to Vue's named slots or Angular's ng-content select. Unlike a single children prop, named slots (like header, footer, actions) let the component define multiple independent injection points for consumers. This gives consumers fine-grained control over what renders in each area while keeping the component's layout structure intact. Card components, modals, and data tables all benefit from this pattern when different zones need independent, separate content.

function Card({ header, body, footer }) {
  return (
    <div className="card">
      <div className="card-header">{header}</div>
      <div className="card-body">{body}</div>
      <div className="card-footer">{footer}</div>
    </div>
  );
}

<Card
  header={<h2>Title</h2>}
  body={<p>Content here</p>}
  footer={<button>Close</button>}
/>

This is more flexible than a single children prop because each slot can receive independent JSX content.

Why it matters: Named slots let consumers place different content in different areas of a component without the component needing to know what that content is.

Real applications: Card components with separate header, body, and footer slots; modal components with a title and actions prop.

Common mistakes: Using a single children prop for everything and then trying to split it — use named props for distinct slots from the start.

Render props were the dominant pattern for sharing logic before React 16.8, but they require wrapping components in extra layers that inflate the component tree and create wrapper hell. Custom hooks solve the exact same problem — sharing stateful logic between components — with no extra JSX nesting, no new component in the tree, and far cleaner syntax. Hooks compose more naturally and are easier to test in isolation. Render props are still appropriate when you need to explicitly delegate rendering responsibility to the consumer, such as in virtualized list libraries.

// Render prop — wraps component, adds nesting
<Mouse>
  {({ x, y }) => <p>{x}, {y}</p>}
</Mouse>

// Custom hook — no extra nesting
function Component() {
  const { x, y } = useMouse();
  return <p>{x}, {y}</p>;
}

Hooks are simpler to compose, test, and read. Use render props only when you need to pass rendering control to the consumer — for example, in a virtualized list library.

Why it matters: Hooks solve the same problems as render props but with less nesting and boilerplate — your JSX stays clean.

Real applications: Replace a <Mouse render={...}> component with a useMouse() hook — less code, same functionality.

Common mistakes: Still using render props for simple data sharing when a custom hook would be cleaner and easier to test.

A component factory is a regular function that creates and returns a React component with pre-baked configuration, avoiding repetition when you need multiple similar components that share core logic but differ in styles or defaults. The factory closes over its configuration parameters, producing each variant as a fully independent component. This is distinct from HOCs — factories don't wrap an existing component, they generate new ones from a shared template. Use this pattern when you have many variants of a component that are all built from the same base.

function createButton(defaultStyle) {
  return function Button({ children, style, ...props }) {
    return (
      <button style={{ ...defaultStyle, ...style }} {...props}>
        {children}
      </button>
    );
  };
}

const PrimaryButton = createButton({ background: 'blue', color: 'white' });
const DangerButton = createButton({ background: 'red', color: 'white' });

Factory functions generate specialized variants without duplicating the core component logic.

Why it matters: You write the base logic once and generate as many variants as you need — changes to the core propagate to all variants automatically.

Real applications: Generating themed buttons (PrimaryButton, DangerButton); creating form field components for different input types.

Common mistakes: Creating too many variants upfront — only generate what you need; prefer props for minor variations over separate factory calls.

A headless component (or headless UI) provides all the behavior, accessibility, and state management for a UI pattern while rendering absolutely no HTML of its own — consumers apply the API to whatever markup they need. This is the ultimate separation of logic from presentation, giving you fully accessible, feature-complete interactive components that look exactly like your design system requires. Libraries like Radix UI, Headless UI, and React Aria are built entirely on this pattern. You get a fully accessible combobox or dialog implemented for free and styled however you like.

// Headless — only logic, no markup
function useAccordion(items) {
  const [openIndex, setOpenIndex] = useState(null);
  const toggle = (i) => setOpenIndex(prev => prev === i ? null : i);
  return { openIndex, toggle };
}

// Consumer — full control over rendering
function MyAccordion({ items }) {
  const { openIndex, toggle } = useAccordion(items);
  return items.map((item, i) => (
    <div key={i}>
      <button onClick={() => toggle(i)}>{item.title}</button>
      {openIndex === i && <p>{item.content}</p>}
    </div>
  ));
}

Libraries like Headless UI and Radix UI are built around this pattern.

Why it matters: Headless components let teams plug in their own design system — the logic is reusable across completely different visual styles.

Real applications: Radix UI provides headless dialog, tooltip, and select components; Downshift provides headless autocomplete behavior.

Common mistakes: Building logic tightly coupled to specific markup — as soon as the design changes you have to rewrite everything.