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:

  1. Event handlers - all handlers are invoked
  2. className strings - concatenated together
  3. style properties - 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 the render prop 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

ParameterTypeDescription
defaultTagNamekeyof React.JSX.IntrinsicElementsDefault tag when render is not provided
renderRenderProp<State>React element or function to override default element
propsRecord<string, unknown>Props to spread on rendered element (merged with internal props)
refReact.Ref | React.Ref[]Refs to apply to rendered element
stateStateComponent state passed to render callback
stateAttributesMappingStateAttributesMapping<State>Custom mapping for state to data-* attributes

Return Value

PropertyTypeDescription
elementReact.ReactElementThe 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

On this page