[RFC] Base UI customization API change #157
Replies: 4 comments 11 replies
-
Would this also introduce changes in the Hooks API? I assume the answer would be a no since it does not inclued components and provide APIs to get props instead but I'd still like some clarification. I have somehow lived under a rock and found about the Base UI completely by accident today and I am really interested in how it progresses |
Beta Was this translation helpful? Give feedback.
-
For the Component replacement API: Is it planned to still be able to easily use simple component replacements without the render pattern? And I have a related question: when we take a custom component as an example - would I pass my custom props in the render function or is the "slot component" then correctly typed and I can pass my props there (like in Material UI)? And how does the render pattern affect potential re-renders/performance? I imagine it's harder to optimize unnecessary re-renders especially since we are just passing in a function. Or would you recommend creating a callback for that? |
Beta Was this translation helpful? Give feedback.
-
Before I say anything, yes, Composition over configuration until you justify that a configuration is granted due to the complexity of a given component. I am not trying to make an argument in favor of Configuration over Composition; I just want to share my thoughts about the topic here. Slot Props
That is why, in practice, the ADR that I wrote (https://github.com/straw-hat-team/adr/blob/master/src/adrs/1358452048/README.md), which you are familiar with, never talked about Configuration vs. CompositionAgain, without arguing in favor of configuration, let's analyze some other existing patterns because I think the most significant pain is the For example, take shadui select component: https://github.com/shadcn-ui/taxonomy/blob/main/components/ui/select.tsx and Radix. Notice that all the "unstyled" components are wrapped with the new classes, properties, and whatnot. In the same way, if you removed the One caveat I also wrote in the ADR is that extra properties that weren't part of the internal control flow, leaning towards HoC, are sometimes needed to create the slots. However, that should be a rare case. These "primitive" components should barely do the HoC because they are closer to the design system than business domain-specific data. Hence, they are genuinely as generic as they get, except for Graphics (CSS) and Semantics (HTML). The real caveat is controlling the order, timing, and locations of the components you inject. For this context, Slots vs. Composition is about "how you are doing the control flow." What most people do not realize from Radix is the massive complexity of everything to be based on "React Context" just because they gave up on the Control Flow. Although the Radix-way composition feels more composable from the outside, that is only an illusion. The user must be careful regarding order, timing, and locations. And from the library author, it is just a matter of React Context everywhere! What that means in practice is that Users will be taking some complexity and Library Authors dealing with every possible composition edge case out there, having to draw the line in a "given configuration" and usage (hah, we are back again to the same place), but just in a "composed" way. If you dig a bit into it, you will find a massive complexity to give this illusion:
Doing things that are proven to be dangerous, such as worrying about the value of With a lot of complexity behind (and slow): Just read that Slot file itself... But whatever they made it work, I think you may be asking for way too much headache as a library maintainer. The users may just need to move away from That is my 2 cents based on my basic understanding of the situation. What I honestly believe is that React itself should come up with a better strategy around having |
Beta Was this translation helpful? Give feedback.
-
@michaldudak Right, I think for me the issue with the // Base UI
function BaseSlider(props) {
const { render, ...other } = props;
return render({
...other,
children: <input type="range" />,
});
}
// Material UI
function Slider() {
const StyledRoot = (props) => (
<span {...props} style={{ background: "red" }} />
);
return <BaseSlider render={(props) => <StyledRoot {...props} />} />;
}
// Actual user
export default function App() {
return (
<div>
<Slider />
</div>
);
} https://codesandbox.io/p/sandbox/base-ui-render-forked-hph4zp?file=%2Fsrc%2FApp.js%3A13%2C54 But then:
|
Beta Was this translation helpful? Give feedback.
-
Context
The current shape of the customization API (
slots
andslotProps
) came to life after discussions within the team and with the community (#21453). We realized, however, that it has its challenges. First, it's not the most ergonomic option when styling with Tailwind CSS or similar libraries. The necessity to use a callback form ofslotProps
makes it too verbose:Additionally,
slots
andslotProps
do not have related types (if you set a slot toMyComponent
, the correspondingslotProps
won't be automatically typed asMyComponentProps
). This was to reduce the work the TypeScript compiler must do when you write your code.Proposal
Component per node
Considering these (and a few other) problems, we decided that in order to make Base UI more developer-friendly, we need to change the customization API.
To start with, we will adopt the "component-per-DOM node" pattern instead of slots/slotProps. This means that each subcomponent that currently is a slot, will be declared explicitly as a subcomponent.
For example, the Select could look roughly like this:
(we're still fleshing out the details and designing the API of each component so details may change)
This is easier to read and reason about, as every component corresponds to a DOM node.
This approach is used by all the other major unstyled libraries, so migrating from them to Base UI will be easier.
Component replacement API
The component-per-node pattern replaces the slotProps, but we still need a way to customize the type of the component rendered in each slot. So far, we've been using the
slots
prop for this, but it doesn't make sense when each slot is represented by a subcomponent.We evaluated several patterns:
as
/component
prop, as in Material UI (<Button component="span" />
)<Button asChild><span /></Button>
<Button render={<span />}
}<Select><Button/><Listbox /></Select>
)<Button render={(props) => <span {...props} />} />
)After considering the pros and cons of each approach, we settled on render props.
This is the most low-level API that could be used to create higher-level APIs in design systems if needed. It also requires to explicitly handle incoming props, which, while more verbose, is safer on the type level). Render props were also considered when we discussed the API in mui/material-ui#21453, but were discarded due to low readability when applied to multiple slots. Now, when we only have one "slot" per component, this is a viable option.
An additional benefit of render props is that they allow to render content depending on the internal state of the component (for example, a different icon in a checked and unchecked Switch)
Feedback
We understand that Base UI, while still unstable, is already used in many projects. Since slots were the primary means of customization, introducing the new API will break these projects.
We would like to know how we can make these changes as smooth for you as possible (while not introducing additional maintenance cost).
We are considering creating a codemod (though we're not sure if we'll be able to cover all the cases reliably with it), creating wrapper components exposing the old API, or documenting how you can create such wrappers on your own.
We are eager to learn what you think about these changes.
Beta Was this translation helpful? Give feedback.
All reactions