useRender
Hook for enabling render props in custom components.
Hook that lets you build custom components with a render prop to override the default rendered element.
Basic Usage
A render prop for a custom Text component lets consumers replace the default rendered p element with a different tag or component.
import { useRender, mergeProps } from "@tilt-legal/cubitt-components/utilities/hooks";
interface TextProps extends useRender.ComponentProps<"p"> {}
function Text(props: TextProps) {
const { render, ...otherProps } = props;
return useRender({
defaultTagName: "p",
render,
props: mergeProps<"p">({ className: "text-sm text-gray-900" }, otherProps),
});
}
// Usage
<Text>Text rendered as a paragraph</Text>
<Text render={<strong />}>Text rendered as a strong tag</Text>Render Callback with State
The callback version of the render prop enables more control of how props are spread, and also passes the internal state of a component.
import { useRender, mergeProps } from "@tilt-legal/cubitt-components/utilities/hooks";
interface CounterState {
odd: boolean;
}
interface CounterProps extends useRender.ComponentProps<
"button",
CounterState
> {}
function Counter(props: CounterProps) {
const { render, ...otherProps } = props;
const [count, setCount] = React.useState(0);
const odd = count % 2 === 1;
const state = React.useMemo(() => ({ odd }), [odd]);
const defaultProps: useRender.ElementProps<"button"> = {
type: "button",
children: (
<>
Counter: <span>{count}</span>
</>
),
onClick: () => setCount((prev) => prev + 1),
};
return useRender({
defaultTagName: "button",
render,
state,
props: mergeProps<"button">(defaultProps, otherProps),
});
}
// Usage with render callback
<Counter
render={(props, state) => (
<button {...props}>
{props.children}
<span>{state.odd ? "odd" : "even"}</span>
</button>
)}
/>;mergeProps
The mergeProps function merges two or more sets of React props together. It safely merges:
- Event handlers - all handlers are invoked
classNamestrings - concatenated togetherstyleproperties - merged into one object
Props are merged left to right, so subsequent objects' properties overwrite previous ones.
import { mergeProps } from "@tilt-legal/cubitt-components/utilities/hooks";
// Using in a render callback
<Component
render={(props, state) => (
<button
{...mergeProps<"button">(props, {
className: "my-custom-class",
})}
/>
)}
/>;Merging Refs
When building custom components, you often need to control a ref internally while still letting external consumers pass their own. The ref option in useRender holds an array of refs to be merged together.
React 19
In React 19, React.forwardRef() is not needed as the external ref prop is already contained inside props:
function Text({ render, ...props }: TextProps) {
const internalRef = React.useRef<HTMLElement | null>(null);
return useRender({
defaultTagName: "p",
ref: internalRef,
props,
render,
});
}React 18 and 17
In older versions of React, use React.forwardRef() and add both refs to the ref array:
const Text = React.forwardRef(function Text(
{ render, ...props }: TextProps,
forwardedRef: React.ForwardedRef<HTMLElement>,
) {
const internalRef = React.useRef<HTMLElement | null>(null);
return useRender({
defaultTagName: "p",
ref: [forwardedRef, internalRef],
props,
render,
});
});TypeScript
Two interfaces are available for typing props:
useRender.ComponentProps<Element, State>- For a component's external (public) props. Types therenderprop and HTML attributes.useRender.ElementProps<Element>- For the element's internal (private) props. Types HTML attributes alone.
interface ButtonProps extends useRender.ComponentProps<"button"> {}
function Button({ render, ...props }: ButtonProps) {
const defaultProps: useRender.ElementProps<"button"> = {
className: "btn",
type: "button",
children: "Click me",
};
return useRender({
defaultTagName: "button",
render,
props: mergeProps<"button">(defaultProps, props),
});
}Types
RenderProp<State>
Generic render prop type. Use this to type a render prop value.
import { RenderProp } from "@tilt-legal/cubitt-components/utilities/hooks";
// For a component with { open: boolean } state
const myRender: RenderProp<{ open: boolean }> = (props, state) => (
<MyCustomElement {...props} isOpen={state.open} />
);
// Or pass a React element directly
const elementRender: RenderProp = <MyCustomElement />;ComponentRenderFn<Props, State>
The function signature for render callbacks.
HTMLProps
HTML props type used in render functions.
API Reference
useRender Parameters
| Parameter | Type | Description |
|---|---|---|
defaultTagName | keyof React.JSX.IntrinsicElements | Default tag when render is not provided |
render | RenderProp<State> | React element or function to override default element |
props | Record<string, unknown> | Props to spread on rendered element (merged with internal props) |
ref | React.Ref | React.Ref[] | Refs to apply to rendered element |
state | State | Component state passed to render callback |
stateAttributesMapping | StateAttributesMapping<State> | Custom mapping for state to data-* attributes |
Return Value
| Property | Type | Description |
|---|---|---|
element | React.ReactElement | The rendered React element |
Migrating from asChild Pattern
Some libraries use an asChild prop with a Slot component. Cubitt uses a render prop instead, which provides clearer semantics and better TypeScript support.
// asChild pattern (other libraries)
<Button asChild>
<a href="/contact">Contact</a>
</Button>
// Cubitt render prop
<Button render={<a href="/contact" />}>Contact</Button>The render prop approach has several advantages:
- Clearer intent - the element to render is explicit
- Better TypeScript inference for props
- Supports render callbacks with component state
- No need for a separate Slot component