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.
One render, start to finish
Section titled “One render, start to finish”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 --> HA 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.
The primitives
Section titled “The primitives”Exported from the package root, for building your own components:
import { useRender, useControlled, mergeProps } from '@arun-dev/headless';| Export | Purpose |
|---|---|
useRender | Resolves what a part renders — merges props and applies render |
useControlled | One value, controlled or uncontrolled, decided at mount |
mergeProps | Merges prop objects: handlers chain, className concatenates, refs merge |
ComponentEvent | Type 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.
Building a part
Section titled “Building a part”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.