Capítulo 51 de 51
The hook that implements the render prop pattern (ch007) for your own custom components — pass a defaultTagName, the consumer's render prop, component state, and merged props, and it returns the finished React element; this is literally what every Base UI part uses internally, and is Base UI's direct answer to Radix's Slot/asChild.
useRender({ defaultTagName: 'p', render, props }) — renders a <p> by default, or whatever render overrides to, with props spread onto it.state (an object) as a third input — it's forwarded as the second argument to a function-form render prop, and Base UI auto-converts state properties to data-* attributes (customizable via stateAttributesMapping, e.g. { isActive: (v) => v ? {'data-is-active': ''} : null }).useRender.ComponentProps<'button'> types a component's external props (includes render); useRender.ElementProps<'button'> types just the internal default HTML props you construct before merging — use the first for your component's exported prop type, the second for the literal defaultProps object inside it.mergeProps(defaultProps, otherProps) (ch050) before passing to props: — never spread manually, or you lose event-handler/className merging.forwardRef needed — pass your internal ref directly as ref: internalRef (or an array [forwardedRef, internalRef] if the component still needs to accept an external ref explicitly, e.g. cross-version code). In React 18/17, wrap the component in React.forwardRef and pass ref: [forwardedRef, internalRef].enabled: false: skips most internal hook logic and returns null — for conditionally-rendered custom components without an extra if wrapper.asChild + <Slot.Root> pattern maps directly to useRender({ defaultTagName, render, props }) — no Slot component needed, render replaces the child-element convention entirely.render is designed primarily for composing behavior/event handlers onto the same default tag. Rendering a genuinely different tag (e.g. swapping a <button> for a <div>) needs an explicit signal for tag-specific default props that wouldn't be valid on the new element (e.g. type="button") — this is exactly why Base UI's own Button exposes nativeButton rather than inferring it from render./* Minimal custom component with a render prop (React 19) */
interface TextProps extends useRender.ComponentProps<'p'> {}
function Text({ render, ...props }: TextProps) {
return useRender({
defaultTagName: 'p',
render,
props: mergeProps<'p'>({ className: styles.Text }, props),
});
}
<Text render={<strong />}>Text as a strong tag</Text>
/* With state passed to a function-form render prop, React 18/17 */
const Text = React.forwardRef(function Text({ render, ...props }: TextProps, forwardedRef) {
const internalRef = React.useRef<HTMLElement | null>(null);
return useRender({
defaultTagName: 'p',
ref: [forwardedRef, internalRef],
props,
render,
});
});
/* Radix asChild equivalent */
// Radix: const Comp = asChild ? Slot.Root : 'button'; return <Comp {...props} />;
// Base UI:
function Button({ render, ...props }) {
return useRender({ defaultTagName: 'button', render, props });
}
<Button render={<MyButton className="primary" />}>Submit</Button>;
render prop, own default tag, own ref-merging) is a ~10-line pattern once useRender + mergeProps are known — this is how to make in-house components compose the same way the library's own parts do.useRender when building an in-house component meant to compose with Base UI conventions (accept render, expose data-* state attributes) — don't hand-roll a parallel asChild-style API.useRender.ComponentProps<Tag>, internal default props with useRender.ElementProps<Tag> — mixing these up is the most common typing mistake when wrapping the hook.ref: internalRef (or an array) directly in React 19, React.forwardRef + ref: [forwardedRef, internalRef] in React 18/17.render prop this hook implements from the component-author side.nativeButton is the real-world example of the "polymorphism needs an explicit signal" caveat above.