as
prop API standard PoC
Polymorphic If an API standard is something that the community is interested in pursuing, it probably makes sense for this to be it's own org run by the community.
I'm a fan of the polymorphic as
prop. I've been using it in React Interactive since 2016 (I think this was the first occurrence), and since then it's seen wide spread use with Styled Components helping to popularize it. With multiple libraries implementing the polymorphic as
prop, I think composability needs to be added to polymorphism (the lack of composability has already caused multiple issues, for example this Stitches issue). A standardized polymorphic API that libraries and components can implement would accomplish this, and would allow multiple polymorphic components to work together seamlessly.
The idea for this is something that I've been thinking about on and off for a bit, and was inspired by this clever styled
function PoC by @jjenzz.
Try out the proof of concept in CodeSandbox
API ideas
The API needs to define a standardized way to:
- Determine at runtime if a component implements the polymorphic API
- Create multilevel polymorphism when the polymorphic API is implemented by both components
- Merge two lines of polymorphism with a create polymorphic function (for example a
styled
function)
Determine at runtime if the API is implemented
In @jjenzz's
styled
PoC she calls the component function with a dummyas
prop, and then checks the returned React Element's type to accomplish this. This is some quality hacking😄 but I don't believe it will scale (there are too many edge cases, e.g. side effects when calling the function component, React components in the form of forwarded ref and memo objects, etc).
The simplest solution is probably to add a property to the component indicating support for the API (the polymorphicAsArray
name will be explained in the next section).
const MyPolymorphicComponent = (/*...*/) => {
/*...*/
};
MyPolymorphicComponent.polymorphicAsArray = true;
// then at runtime in some other component we can do
if (SomeComponent.polymorphicAsArray === true) {
// SomeComponent implements the polymorphic API
}
Create multilevel polymorphism
Polymorphic components can support multilevel polymorphism by accepting an array for the as
prop.
- A component that implements the
polymorphicAsArray
API would need to:- Accept an array for the
as
prop - Remove the first item from the
as
array and check if it supports thepolymorphicAsArray
API, if yes render it and pass through the remainingas
array. If it doesn't implement the API, discard it, and check the next item. Repeat until either it finds a component that supports thepolymorphicAsArray
API, or it gets to the last item in the array, then always render it. - Or render a predefined component that supports the
polymorphicAsArray
API and pass through theas
prop to that component untouched. - Set the
polymorphicAsArray
property on the component totrue
. - Implement additional requirements of the API like
ref
forwarding (not done in the below example).
- Accept an array for the
Note that the PoC npm package exports a helper function polymorphicAsArrayUtil
that implements the as
array logic.
const PolymorphicComponent = ({ as = 'div', ...props }) => {
let As;
let asArray = [];
if (!Array.isArray(as)) {
As = as;
} else {
asArray = [...as];
while (asArray.length > 0) {
const item = asArray.shift();
if (
// item implements polymorphicAsArray API
item?.polymorphicAsArray === true ||
// item is the last item in the `as` array
asArray.length === 0
) {
As = item;
break;
}
}
}
// component logic...
// if the asArray is empty, don't pass it through
const passThroughAsProp = asArray.length === 0 ? undefined : asArray;
return <As {...props} as={passThroughAsProp} />;
};
PolymorphicComponent.polymorphicAsArray = true;
// usage
// note that ButtonBase is also a polymorphic component
const App = () => (
<MyPolymorphicComponent as={[ButtonBase, 'a']} href="#polymorphic">
This renders MyPolymorphicComponent as a ButtonBase as an anchor tag
</MyPolymorphicComponent>
);
Merge two lines of polymorphism with a create function
A create polymorphic function can come in different flavors for different use cases with additional functionality. For example, a
styled
function that implements thepolymorphicAsArray
API would be a create function. Also, a create function can optionally support a default props argument (using composition, not the to-be-deprecateddefaultProps
property).
- A create polymorphic function that implements the
polymorphicAsArray
API and merges two lines of polymorphism would need to:- Accept an
as
array as the first argument (additional arguments can be supported for specific functionality, e.g. a styles object, default props object, etc). - Return a component that:
- Accepts an array for the
as
prop. - Appends the
as
prop to the end of theas
array argument passed to the create function. - Renders the merged
as
array as describe above in Create multilevel polymorphism (render the first item that supports thepolymorphicAsArray
API and pass through the remaining array). - Has the
polymorphicAsArray
property set totrue
. - Implements additional requirements of the API like
ref
forwarding (not done in below example).
- Accepts an array for the
- Accept an
Note that the PoC npm package exports a helper function polymorphicAsArrayUtil
that implements the as
array logic.
const createPolymorphic = (defaultAs = 'div') => {
const asArray = Array.isArray(defaultAs) ? [...defaultAs] : [defaultAs];
// polymorphic component that's returned from the create function
const PolymorphicComponent = ({ as, ...props }) => {
if (as !== undefined) {
// append the `as` prop to the end of the `defaultAs` argument
Array.isArray(as) ? asArray.push(...as) : asArray.push(as);
}
// same logic as in the above "Create multilevel polymorphism" example
let As;
while (asArray.length > 0) {
const item = asArray.shift();
if (
// item implements polymorphicAsArray API
item?.polymorphicAsArray === true ||
// item is the last item in the `as` array
asArray.length === 0
) {
As = item;
break;
}
}
const passThroughAsProp = asArray.length === 0 ? undefined : asArray;
return <As {...props} as={passThroughAsProp} />;
};
PolymorphicComponent.polymorphicAsArray = true;
return PolymorphicComponent;
};
// usage
// note that Interactive is also a polymorphic component
const ButtonBase = createPolymorphic([Interactive, 'button']);
const ButtonLink = createPolymorphic([ButtonBase, 'a']);
const ButtonLinkWithClosedApi = ({ disabled = false, href }) => (
<ButtonLink
as={disabled ? 'span' : undefined}
href={disabled ? undefined : href}
/>
);
Proof of concept npm package
The polymorphic-as
proof of concept npm package exports 3 functions:
-
polymorphicAsArrayUtil
handles the logic of mergingas
arrays, selecting theAs
to render, and creating thepassThroughAsProp
. -
createPolymorphic
is a generic create polymorphic function that accepts a default props object. -
styled
is a create polymorphic function that composes component styles.
Install
npm i --save polymorphic-as
polymorphicAsArrayUtil
Helper function to handle the logic of merging as
arrays, selecting the As
to render, and creating the passThroughAsProp
.
polymorphicAsArrayUtil
should be used by every component and create function that implements the polymorphicAsArray
API.
import { polymorphicAsArrayUtil } from 'polymorphic-as';
const AsAndPassThroughAsProp = polymorphicAsArrayUtil({
defaultAs, // the `as` argument from a create polymorphic function, optional
as, // the `as` prop passed to a polymorphic component, optional
});
const {
As, // <As /> to render
passThroughAsProp, // `as` prop to pass to As, <As as={passThroughAsProp} />
} = AsAndPassThroughAsProp;
// using polymorphicAsArrayUtil in a polymorphic component
import { polymorphicAsArrayUtil } from 'polymorphic-as';
const MyPolymorphicComponent = React.forwardRef(
({ as = 'div', ...props }, ref) => {
const { As, passThroughAsProp } = polymorphicAsArrayUtil({ as });
// component logic...
return <As {...props} as={passThroughAsProp} ref={ref} />;
},
);
MyPolymorphicComponent.polymorphicAsArray = true;
// using polymorphicAsArrayUtil in a create function
export const myPolymorphicCreateFunction = (defaultAs) => {
// the polymorphic component that's returned from the create function
const PolymorphicComponent = React.forwardRef(({ as, ...props }, ref) => {
const { As, passThroughAsProp } = polymorphicAsArrayUtil({
defaultAs,
as,
});
return <As {...props} as={passThroughAsProp} ref={ref} />;
});
PolymorphicComponent.polymorphicAsArray = true;
return PolymorphicComponent;
};
createPolymorphic
createPolymorphic
is a generic create function that returns a polymorphic component which implements the polymorphicAsArray
API. It also accepts a default props object as a second argument.
import { createPolymorphic } from 'polymorphic-as';
const PolymorphicComponent = createPolymorphic(as | as[], defaultPropsObject);
import { createPolymorphic } from 'polymorphic-as';
const MyButton = createPolymorphic('button', { className: 'my-button' });
const MyButtonLink = createPolymorphic([MyButton, 'a']);
styled
Adapted from this
styled
function PoC by @jjenzz.
The intention of the
styled
function PoC is to demonstrate what's possible if a productionstyled
function (e.g. Stitches'styled
) implemented thepolymorphicAsArray
API.
styled
is a create function that composes component styles (using inline styles) and returns a styled polymorphic component. It also accepts a default props object as a third argument.
import { styled } from 'polymorphic-as';
const StyledPolymorphicComponent = styled(as | as[], stylesObject, defaultPropsObject);
// button example
import { styled } from 'polymorphic-as';
const ButtonBase = styled('button', {
padding: '10px 20px',
border: '1px solid',
borderRadius: '1000px',
});
const ButtonLink = styled([ButtonBase, 'a']);
const GreenButton = styled(ButtonBase, { color: 'green' });
const SubmitButton = styled(
GreenButton,
{ fontWeight: 'bold' },
{ type: 'submit' },
);
const TopOfPageButton = styled(
[ButtonBase, 'a'],
{ padding: '5px 10px' },
{ href: '#top', children: 'Top of Page' }, // default props
);
// text input example
import { styled } from 'polymorphic-as';
const TextInput = styled(
'input',
{ border: '1px solid', borderRadius: '4px' },
{ type: 'text' }, // default props
);
TypeScript
The proof of concept code is written is TypeScript but with a generous use of any
(there are no inferred types from the as
prop).
The polymorphic-as
package does export a PolymorphicAsArrayForwardRefComponent
type that can be used to type components that implement the polymorphic as array API, and a PolymorphicAsArrayProps
type that the props interface can extend.
import {
polymorphicAsArrayUtil,
PolymorphicAsArrayForwardRefComponent,
PolymorphicAsArrayProps,
} from 'polymorphic-as';
interface MyPolymorphicComponentProps extends PolymorphicAsArrayProps {
someProp?: string;
}
// prettier-ignore
const MyPolymorphicComponent:
PolymorphicAsArrayForwardRefComponent<MyPolymorphicComponentProps> =
React.forwardRef(
({ as = 'div', someProp, ...props }, ref) => {
const { As, passThroughAsProp } = polymorphicAsArrayUtil({ as });
// component logic...
return <As {...props} ref={ref} />;
},
);
MyPolymorphicComponent.polymorphicAsArray = true;