Prop merging
Three parties want to put props on the same element: the component, you, and any element you pass
to render. Naive spreading lets whichever runs last silently destroy the others, so each kind of
prop is combined rather than replaced.
Precedence
Section titled “Precedence”flowchart TB A["1 · the component's own props<br/><small>props, including its data-* state</small>"] --> B["2 · your props<br/><small>consumerProps</small>"] B --> C["3 · props on the render element<br/><small>cloneElement</small>"] C -.->|"plain values: last wins"| V["className concatenated<br/>style shallow-merged<br/>refs merged<br/>children replaced"] C -.->|"handlers: run in reverse"| H["3 → 2 → 1<br/>stop with preventComponentHandler()"] classDef out fill:transparent,stroke-dasharray:4 3 class V,H out
A component’s data-* state attributes are part of its own props, at the bottom of the order, so
you can override one when you have a reason to.
The order is not a convention components follow — useRender takes the component’s props and
yours in separate arguments and merges them itself, so there is no arrangement a component author
could get wrong.
| Prop | How it combines |
|---|---|
className | concatenated, never replaced |
style | shallow-merged, per property |
ref | merged — every ref receives the node, and every cleanup runs |
| event handlers | chained, in reverse precedence order |
children and everything else | last one wins |
undefined is skipped, so an absent prop never clobbers a value someone else set.
Children follow the plain-value rule
Section titled “Children follow the plain-value rule”children is not special-cased, which means a render element that declares its own content
keeps it — and one that declares none inherits yours:
{ /* renders "Docs" — the span has no children, so yours survive */}<Button render={<span />}>Docs</Button>;
{ /* renders "Read the docs" — the span's own children are the last word */}<Button render={<span>Read the docs</span>}>Docs</Button>;Both are useful, and neither is a special case: an absent children key is undefined and gets
skipped, a present one wins like any other value.
Handlers run in reverse
Section titled “Handlers run in reverse”Values resolve so that yours beat the component’s. Handlers do the opposite: yours runs first, and can stop the component’s from running at all.
<Switch.Root onClick={(event) => event.preventComponentHandler()} />The escape hatch belongs to you. A component already decides which props reach the element, so it never needs the chain to suppress anything — it simply declines to attach a handler. Without this you could not adapt a component’s behaviour without forking it.
preventDefault() is deliberately left alone. It already means “cancel the browser’s default
action”, which is a different statement — a link inside a component may want one without the
other.
Only real events carry the signal. An on* prop called with something else — onCheckedChange
receives a boolean — always runs every handler, since there is nothing to attach it to.
A ref on the component and a ref on the render element both receive the node:
<Switch.Root ref={mine} render={<Tooltip.Trigger ref={theirs} />} />Both are called on attach, and both are cleaned up on detach — whether they use React 19’s
cleanup return or the older null call.