Back to React
React
medium
mid

How would you expose a child component's API to its parent in React?

Prefer declarative props (controlled value + event callbacks) over imperative APIs. When imperative access is genuinely needed (focus, scroll, play), expose a minimal, intentional handle via forwardRef + useImperativeHandle. Compound components for structural APIs.

6 min read·~12 min to think through

"Exposing an API for parents" means designing how a parent controls and observes a child. The default should be declarative; imperative is the escape hatch.

1. Declarative first — props and callbacks

Most component APIs should be:

  • Inputs: props (value, open, disabled, items).
  • Outputs: event callbacks (onChange, onOpen, onSelect).
  • Controlled / uncontrolled: support both — value/onChange for controlled, defaultValue for uncontrolled, like native inputs.

This keeps state flow predictable: data down, events up. The parent drives the child by re-rendering it with new props.

2. Imperative handle — only when declarative can't express it

Some actions are inherently imperative: focus(), scrollToIndex(), play(), open(), resetForm(), validate(). For these, expose a deliberate, minimal handle:

jsx
const Modal = forwardRef(function Modal(props, ref) {
  const [open, setOpen] = useState(false);
  useImperativeHandle(ref, () => ({
    open: () => setOpen(true),
    close: () => setOpen(false),
  }), []);
  // ...
});

// parent:
const modalRef = useRef(null);
modalRef.current?.open();

Rules: expose named methods, not the raw DOM node; keep the surface tiny; only methods that genuinely can't be props.

3. Compound components — structural APIs

For components with parts, expose composition instead of a giant prop object:

jsx
<Tabs value={tab} onChange={setTab}>
  <Tabs.List>
    <Tabs.Tab value="a">A</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel value="a">…</Tabs.Panel>
</Tabs>

Flexible, readable, and the parent controls structure without prop explosions.

4. Render props / children-as-function

When the parent needs to control rendering with the child's internal state:

jsx
<Downshift>{({ isOpen, getItemProps }) => <ul>…</ul>}</Downshift>

Design principles

  • Declarative by default, imperative by exception.
  • Minimal surface — every prop/method is a maintenance and docs cost.
  • Predictable — follow native-element conventions (value/onChange, defaultValue).
  • Don't leak internals — expose intent (open()), not implementation (the DOM node, internal state setters).

Follow-up questions

  • When is useImperativeHandle the right call vs a prop?
  • How do you support both controlled and uncontrolled usage?
  • What are the tradeoffs of compound components vs a big props object?
  • Why is exposing the raw DOM node via ref usually a bad idea?

Common mistakes

  • Reaching for imperative refs when a prop would do.
  • Exposing the raw DOM node or internal setters instead of intentful methods.
  • Giant prop objects instead of composition.
  • Not supporting controlled mode, forcing parents to fight the component's internal state.

Performance considerations

  • Declarative props re-render predictably. Imperative handles can bypass React's data flow and hide state changes — use sparingly. Compound components use context; keep that context value stable to avoid re-rendering all parts.

Edge cases

  • Parent needs to both control and observe — controlled props + callbacks.
  • Imperative method called before the child has mounted.
  • Compound components where children are wrapped/reordered (context-based, not index-based).
  • Ref forwarding through multiple wrapper layers.

Real-world examples

  • A <VideoPlayer> exposing play()/pause() via useImperativeHandle while volume/src are props.
  • Radix/Headless UI style compound components for menus, tabs, dialogs.

Senior engineer discussion

Seniors articulate the declarative-default / imperative-exception principle and design minimal, intent-revealing APIs. They reach for compound components and render props for structural flexibility, support controlled+uncontrolled like native elements, and treat every exposed prop/method as an ongoing API contract cost.

Related questions