adobe / react-spectrum

A collection of libraries and tools that help you build adaptive, accessible, and robust user experiences.
https://react-spectrum.adobe.com
Apache License 2.0
13.11k stars 1.14k forks source link

asChild props, like in Radix UI #5321

Open zxti opened 1 year ago

zxti commented 1 year ago

Provide a general summary of the feature here

Radix UI supports the asChild prop, which lets you provide a complete other component to serve in a certain role.

This would let you much more easily adopt React Aria Components within codebases that already have other components (but also some gotchas that users just have to be aware of). Especially for things like Buttons, etc.

๐Ÿค” Expected Behavior?

See the Radix docs: https://www.radix-ui.com/primitives/docs/guides/composition

๐Ÿ˜ฏ Current Behavior

You are limited to just using React Aria Components with itself.

๐Ÿ’ Possible Solution

No response

๐Ÿ”ฆ Context

Has been brought up as feedback in the past:

https://github.com/adobe/react-spectrum/issues/2331#issuecomment-920957954

https://news.ycombinator.com/item?id=35853692

๐Ÿ’ป Examples

No response

๐Ÿงข Your Company/Team

No response

๐Ÿ•ท Tracking Issue

No response

devongovett commented 1 year ago

I'd recommend reading through our Advanced customization guide, in particular the section about consuming contexts. All of the contexts we provide are exported, so you can consume them in your own components. This makes it possible to reuse existing components you may have, either by modifying them to consume those contexts internally or by creating a small wrapper.

For buttons in particular, we do rely on the normalization done by React Aria hooks internally, so when a component contains a button it expects that this is happening. Therefore using an arbitrary element would not be enough, it would need to include the hooks too. But, if you want to customize the element you can drop down to the hook API yourself, and as long as it consumes from ButtonContext it will work with other React Aria components. See this example from the button docs.

We've discussed APIs like as and asChild before, but we think they are too blunt an instrument. It's extremely powerful, but it's too easy to break the accessibility, behavior, interactions, or types of a component, and supporting this would prevent us from being able to make internal changes in the future. Usually there is a better way to solve individual problems that might have been solved by these tools in the past, such as sharing styles (reuse the same CSS class names), behavior, etc. But if there's something in particular you've come across that isn't already possible to solve please let us know.

aspirisen commented 8 months ago

I think it is worth to add asChild prop. It is not necessary when designs are matching adobe's one. But react area components are supposed to be applied to various scenarios, even to those which were not built in into the library. Sometimes it is much easier and more flexible to use different component, but with the same functionality.

I've used as prop in styled-components for a while, and find it less usable rather than asChild. With asChild you do not need to implement complex type inference, the props types are still simple as before. Also, with asChild you can combine behavior of several components into one.

devongovett commented 8 months ago

With asChild you do not need to implement complex type inference

asChild is not type safe, that's why it seems simpler. You can put any component in there even if it doesn't accept the right props or render the right element and it'll just be broken at runtime instead.

What are you trying to achieve? There's usually another way to do it. Happy to help if you have a specific case. Make sure to read the guide linked above as well.

aspirisen commented 8 months ago

@devongovett

asChild is not type safe, that's why it seems simpler. You can put any component in there even if it doesn't accept the right props or render the right element and it'll just be broken at runtime instead.

I feel that asChild has the same level of type safety as as prop. In the component in children of asChild typescript still performs type checking as usual. Also, asChild work better with generic components, if you use as prop there can be difficulties with correct type inference of generic components in as prop. And as prop requires more complex typescript computations - I noticed that when I moved to asChild approach autocompletion has become much faster

What are you trying to achieve? There's usually another way to do it. Happy to help if you have a specific case. Make sure to read the guide linked above as well.

Mostly for styling, I have several components that covers specific area of css - Flex, Grid, Stack, Paint, Text etc. And then combine styles that I need by using asChild. I am just trying to avoid using all-in-one Box component, the same as giving control to all css properties to all components.

devongovett commented 8 months ago

I feel that asChild has the same level of type safety as as prop.

In TypeScript you can't really specify the types of React children. Here's an example. As you can see, Parent should only accept children with a foo prop that is a string, but TypeScript actually allows any child with a different type, or even a random div that doesn't accept a foo prop at all. That is why I say children are untyped, so therefore asChild is untyped. Accepting arbitrary children can cause runtime errors, broken behavior, accessibility issues, and other problems.

Mostly for styling, I have several components that covers specific area of css - Flex, Grid, Stack, Paint, Text etc.

I think another approach to this is to share the styles rather than components. That way they can be reused across different components. This example uses inline styles but you could use tailwind, css-in-js, etc. to achieve the same result.

function flex(options) {
  return {display: 'flex', gap: options.gap, flexDirection: options.direction, /* ... */ };
}

<ReactAriaComponent style={flex({...})} />

This approach is more granular - you are explicit about what is being shared (only styles, no events, or other hidden behaviors), and you have control over how multiple of these are merged together (with asChild you don't have control over how props from multiple components are merged). This also makes it easier to share styles between different raw DOM elements too, rather than being locked into a <div> being rendered by <Flex> or needing an as/asChild prop there too.

Another way to think about this is that components should be responsible for the behavior, semantics, and elements that are rendered, and styling should be passed into them. The component knows what is represents โ€“ a button, checkbox, switch, etc. โ€“ so it needs to control the rendered elements and behaviors. asChild flips this around so behaviors get passed down to arbitrary elements (which may also have other conflicting behaviors of their own), easily resulting in unexpected issues and bugs. Passing styles into components avoids these issues and results in more predictable behavior and fewer potential accessibility issues.

aspirisen commented 6 months ago

@devongovett thanks for detailed explanation.

I wanted to emphasize that asChild prop is better than as prop approach in my opinion. But, yes there is no way to type react children. I just think that asChild can be used for composition because of server components support.

Also about passing functions to style components, I thought I tried this approach, but after that all my JSX turned into divs and spans what was harder to read than named jsx tags like Flex

jrmyio commented 1 month ago

I recently ran into issues styling the <CalendarCell/>. Its rendering a <td><div> and there is no way to target the <td> or give it any attributes or classnames.

Having worked for many months on a design system based on RAC I can say not having access to the wrapper is the probably the number 1 friction point I experienced. In order to allow the behavior I was left with forking RAC or copying individual components, causing all sorts of issues with component contexts or utils not being exported.

I like the renderPropspattern in react-aria-component but it would be awesome if components allow you to opt out of the wrapper being rendered and have the HTML attributes be passed to the renderProps's object. This could be introduced as a non breaking change as far as I can see.