Polymorphic components #1835
NicholasBoll
started this conversation in
General
Replies: 2 comments 1 reply
-
This is a great write up Just had a question if we do use the <Modal.Target model={model}>
<PrimaryButton aria-label="Open" icon={dropdownIcon} />
</Modal.Target> Would the target need |
Beta Was this translation helpful? Give feedback.
1 reply
-
The more I think about this, the more I like the import React from 'react';
import { Slot } from '@radix-ui/react-slot';
function Button({ asChild, ...props }) {
const Comp = asChild ? Slot : 'button';
return <Comp {...props} />;
}
// usage
<Button asChild>
<a href="/contact">Contact</a>
</Button> |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Problem
All of our components created with
createComponent
,createContainer
, andcreateSubcomponent
are polymorphic. This means the rendered element/component can be changed via theas
prop.For example:
Polymorphism isn't a strong suit of Typescript. Currently, we achieve this polymorphism via a function overloading. Function overloading means Typescript will check for an overload without the
as
prop and one with theas
prop when checking props. Function overloads are quite fast, so real-time type checking is responsive. The problem is that function overloads give type errors for each overload:Expand for the full text of the error
Also notice from the screenshot, the whole
Modal.Target
JSX component is underlined. This is because Typescript doesn't know how to narrow it down. According to Typescript, the error is"No overload matches this call"
and not "nonExistantProp
does not exist onModal.Target
". A user can parse the overload errors to understand the issue, but it is very difficult to parse. Also the underline doesn't narrow down the issue.Ideally, this is the error we get:
Expand for the full text of the error
Solutions
Solution 1
Remove the overload. This is what I did for the screenshot. The problem I ran into was crashing the Typescript compiler. Typescript is trying to resolve an overload type. For example:
The problem is Typescript tries to resolve
EOverride
all at once instead of only whenas
is defined: (ref?: ExtractRef<EOverride>
,as?: EOverride
, andExtractProps<EOverride, P>
).ExtractProps
tries to determine the type ofEOverride
to decide what prop interface should be returned (P & ExtractHTMLAttributes<E>
ifas
is not defined andP & Omit<React.ComponentProps<EOverride, 'as' | keyof P>> & ExtractHTMLAttributes<EOverride>
ifas
is defined). This complexity causes Typescript to fail with an error code 137 (out of memory or timeout).I can get Typescript to respond if I limit the allowable types of
as
to only element strings or anotherElementComponent
(Currently, any React component is allowed). This works, but is much slower than the overloads.Typescript error no overload:
Typescript error no overload.webm
Typescript error with overload:
Typescript error with overload.webm
The videos show the overload recognizing the error almost immediately (there's a small delay in both videos where Typescript is waiting for the user to stop inputting). The non-overload takes about 2 seconds. The non-overload also has a wider compute time, decreasing the editor's responsiveness. I have a very fast machine and my editor feels sluggish with this change.
Solution 2
Change polymorphism. Radix changed from an
as
prop toasChild
.Twitter thread: https://twitter.com/jjenzz/status/1499301618843586562
Pull Request: radix-ui/primitives#835
The change still gets the per-prop error message. The benefit is Typescript resolves types very quickly - there is no complicated inference to process.
This approach works by using
React.cloneElement
on the immediate child ifasChild
is present.The downside to this approach is loss of type checking between the host interface (
Modal.Target
) and the rendered component (DeleteButton
). This is most likely not much of a problem in practice. One thing that is a little confusing is where extra props go.It is a little unclear where props go. This could be worse if the default component a host component represents needs a required property.
I don't think this would happen much in practice. Almost none of our components have required props that would be an issue, but it could be unexpected.
Radix does not have style prop primitives like Canvas Kit does. The current solution of a paragraph with
BoxProps
would look like this:This wouldn't work since
Box
is actually a styled component. Theas
prop would still need to be supported, but would fall back to a HTML tag only or not make the interface polymorphic. In order to support theas
prop for these instances and still keep the performance, theas
would need to not change the prop interface. This is a little strange:The current polymorphic
as
changese
toReact.MouseEventHandler<HTMLAnchorElement>
. This update would need to leave the interface alone which can cause problems when people passref
s and event handlers. They have to pass abutton
interface even though an anchor is rendered.Beta Was this translation helpful? Give feedback.
All reactions