

Hook that lets you build custom components with a `render` prop to override the default rendered element.

## Basic Usage [#basic-usage]

A `render` prop for a custom Text component lets consumers replace the default rendered `p` element with a different tag or component.

```tsx
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 [#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.

```tsx
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` [#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.

```tsx
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 [#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 [#react-19]

In React 19, `React.forwardRef()` is not needed as the external ref prop is already contained inside `props`:

```tsx
function Text({ render, ...props }: TextProps) {
  const internalRef = React.useRef<HTMLElement | null>(null);

  return useRender({
    defaultTagName: "p",
    ref: internalRef,
    props,
    render,
  });
}
```

### React 18 and 17 [#react-18-and-17]

In older versions of React, use `React.forwardRef()` and add both refs to the `ref` array:

```tsx
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 [#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.

```tsx
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 [#types]

### `RenderProp<State>` [#renderpropstate]

Generic render prop type. Use this to type a `render` prop value.

```tsx
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>` [#componentrenderfnprops-state]

The function signature for render callbacks.

### `HTMLProps` [#htmlprops]

HTML props type used in render functions.

***

## API Reference [#api-reference]

### `useRender` Parameters [#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 [#return-value]

| Property  | Type                 | Description                |
| --------- | -------------------- | -------------------------- |
| `element` | `React.ReactElement` | The rendered React element |

***

## Migrating from `asChild` Pattern [#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.

```tsx
// 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
