Skip to content

Commit

Permalink
Simplify hook & docs page
Browse files Browse the repository at this point in the history
  • Loading branch information
mnajdova committed Feb 25, 2025
1 parent cd4b762 commit 0e16678
Show file tree
Hide file tree
Showing 9 changed files with 15 additions and 330 deletions.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

60 changes: 2 additions & 58 deletions docs/src/app/(public)/(content)/react/utils/use-render/page.mdx
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
# useRender

<Subtitle>Utility for adding Base UI-like features to custom components.</Subtitle>
<Subtitle>Utility for creating custom components that support a render prop.</Subtitle>
<Meta name="description" content="Utility for adding Base UI-like features to custom components." />

The `useRender` hook allows you to support the same features Base UI provides across all components so that you can also have a consistent experience in your custom components:

- A [render](/react/handbook/composition) prop to override the default rendered element (similar to `asChild` from Radix).
- [data attributes](https://base-ui.com/react/handbook/styling#data-attributes) that map to the component's state which can be used for styling.
- A [callback](/react/handbook/styling#css-classes) on the `className` prop that enables passing CSS classes depending on the component's state.
The `useRender` hook allows you to support the `render` prop to override the default rendered element (similar to `asChild` from Radix) in your custom components.

## API reference

Expand All @@ -24,20 +20,6 @@ The `useRender` hook allows you to support the same features Base UI provides ac
type: 'Record<string, unknown>',
description: 'Props to be spread on the rendered element.',
},
className: {
type: 'string | ((state: State) => string)',
description:
"CSS class applied to the element, or a function that returns a class based on the component's state.",
},
state: {
type: 'State',
description:
'The state of the component. It will be used as a parameter for the render and className callbacks.',
},
stateAttributesMap: {
type: 'StateAttributes<State>',
description: 'A mapping of state to data attributes.',
},
}}
/>

Expand All @@ -47,48 +29,10 @@ The hook returns a function that when called returns the element that should be

## Examples

### Support the `render` prop

This is an example of a Text component supporting the `render` prop.

<Demo path="./demos/render" />

### State-based data attributes

When you pass the `state` option, data attributes based on this state are added automatically. In the following example, the counter text color is set to red when it's an odd number by targeting the corresponding data attribute.

<Demo path="./demos/data-attributes" />

To customize how data attributes are generated, use the `stateAttributesMap` option.

```jsx {4-6} title="Customizing data attributes"
useRender({
render: render ?? <button />,
state,
stateAttributesMap: {
odd: (value) => (value ? { ['data-parity']: 'odd' } : { ['data-parity']: 'even' }),
},
});
```
To skip a data attribute, return `null` in the corresponding state key.
```jsx {5} title="Skipping data attributes"
useRender({
render: render ?? <button />,
state,
stateAttributesMap: {
odd: () => null,
},
});
```
### State-based class names
You can define a `className` prop that accepts a callback function, enabling dynamic CSS class assignment based on the component's state. The example below achieves the same result as the state-based attributes approach but uses the `className` callback for styling instead of data attributes.
<Demo path="./demos/class-name" />
### Migrating from Radix

Radix and Base UI take different approaches to customizing the rendered element within a component. Radix uses the `asChild` prop, while Base UI uses the `render` prop. Learn more about how composition works in Base UI in the [composition guide](/react/handbook/composition).
Expand Down
70 changes: 6 additions & 64 deletions packages/react/src/use-render/useRender.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ describe('useRender', () => {

it('render props does not overwrite className in a render function when unspecified', async () => {
function TestComponent(props: {
render: useRender.Settings<any, Element>['render'];
className?: useRender.Settings<any, Element>['className'];
render: useRender.Settings<Element>['render'];
className?: string;
}) {
const { render: renderProp, className } = props;
const { renderElement } = useRender({
Expand All @@ -22,72 +22,14 @@ describe('useRender', () => {

const { container } = await render(
<TestComponent
render={(props: any, state: any) => <span className="my-span" {...props} {...state} />}
render={(props: any, state: any) => (
<span {...props} className={`my-span ${props.className ?? ''}`} {...state} />
)}
/>,
);

const element = container.firstElementChild;

expect(element).to.have.attribute('class', 'my-span');
});

it('includes data-attributes for all state members', async () => {
function TestComponent(props: {
render?: useRender.Settings<any, Element>['render'];
className?: useRender.Settings<any, Element>['className'];
size: 'small' | 'medium' | 'large';
weight: 'light' | 'regular' | 'bold';
}) {
const { render: renderProp, size, weight } = props;
const { renderElement } = useRender({
render: renderProp ?? 'span',
state: {
size,
weight,
},
});
return renderElement();
}

const { container } = await render(<TestComponent size="large" weight="bold" />);

const element = container.firstElementChild;

expect(element).to.have.attribute('data-size', 'large');
expect(element).to.have.attribute('data-weight', 'bold');
});

it('respects the customStyleHookMapping config if provided', async () => {
function TestComponent(props: {
render?: useRender.Settings<any, Element>['render'];
className?: useRender.Settings<any, Element>['className'];
size: 'small' | 'medium' | 'large';
weight: 'light' | 'regular' | 'bold';
}) {
const { render: renderProp, size, weight } = props;
const { renderElement } = useRender({
render: renderProp ?? 'span',
state: {
size,
weight,
},
stateAttributesMap: {
size(value) {
return { [`data-size${value}`]: '' };
},
weight() {
return null;
},
},
});
return renderElement();
}

const { container } = await render(<TestComponent size="large" weight="bold" />);

const element = container.firstElementChild;

expect(element).to.have.attribute('data-sizelarge', '');
expect(element).not.to.have.attribute('data-weight');
expect(element).to.have.attribute('class', 'my-span ');
});
});
Loading

0 comments on commit 0e16678

Please sign in to comment.