How would you expose the API for parent components
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.
"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/onChangefor controlled,defaultValuefor 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:
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:
<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:
<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.