Skip to content

The render engine

Every part — each Switch.Root, each Switch.Thumb — renders through one function, useRender. That is what makes the library consistent: one place decides how props combine and what element comes out.

flowchart LR
  A["Component<br/><small>own props incl. data-*, consumer props</small>"] --> B["useRender"]
  B --> D["mergeProps<br/><small>combine, in precedence order</small>"]
  D --> E{"render prop<br/>supplied?"}
  E -- no --> F["createElement<br/><small>default tag</small>"]
  E -- yes --> G["cloneElement<br/><small>merge onto your element</small>"]
  F --> H["DOM"]
  G --> H
A part describes what it wants; useRender decides what is actually rendered.

A component supplies two things — the props it wants on the element, including any data-* that carry its state, and a default tag. It never calls createElement itself.

That indirection is the point. If each component merged its own props, each would be free to get the precedence wrong, and the twelfth component would behave subtly differently from the first.

Exported from the package root, for building your own components:

import { useRender, useControlled, mergeProps } from '@arun-dev/headless';
ExportPurpose
useRenderResolves what a part renders — merges props and applies render
useControlledOne value, controlled or uncontrolled, decided at mount
mergePropsMerges prop objects: handlers chain, className concatenates, refs merge
ComponentEventType for a handler that can call preventComponentHandler()

Everything this library uses to implement its own components stays private, so it can change without breaking anyone. The public surface is the table above plus the components themselves.

import { useRender } from '@arun-dev/headless';
function Panel({ open, className, children, render, ...rest }) {
return useRender({
render,
defaultTagName: 'section',
props: {
// State reaches CSS as plain `data-*` props. React drops the `undefined` ones,
// so there is no mapping layer between the state and the DOM.
'data-open': open ? '' : undefined,
'data-closed': open ? undefined : '',
className,
children, // yours
},
consumerProps: rest, // the caller's
});
}

<Panel open /> renders <section data-open class="…">, accepts a render prop, and passes id, aria-* and event handlers through — without that component writing any of it.

Note the two separate slots. useRender merges them in a fixed order, so a component cannot place a caller’s props before its own even by accident — the precedence on the next page is a property of this signature rather than a rule to remember.