as-with
A re-usable pattern for rendering a React component through another React component.
Why
It can be tedious writing higher order components (HOC) to combine functionality of simple components.
For example, a UI Framework such as rbx – which provided the initial implementation of this code – largely transforms components' props into a className
prop which is then used to style a DOM element.
import { Button } from "rbx";
// renders: <button className="button is-primary">The button</button>
const myButton = <Button color="primary">The button</Button>;
// renders: <a className="button is-primary">The link</a>
const myLink = (
<Button as="a" color="primary">
The Link
</Button>
);
Of course, simple components (like the naïve <Button>
implementation above) should also forward ref
s – those handy React objects that let you manipulate components or the underlying JSX elements.
There are a handful of constraints in this problem-space, that enable sane usage:
- a component should be able to render as any other component (for example,
as={MyComponent}
) - the receiving component must accept whatever props the providing component passes (for example,
MyComponent
should acceptclassName: string
if used as theas
prop for<Button>
) - all unconsumed props should be forwarded to the underlying
as
component. - if one of the required props for the component provided to
as
conflicts with an optional or required prop of the base component (for example,<Button>
takes an optional propcolor
and perhaps<MyComponent>
takes a required propcolor
), then the base-component (e.g.<Button>
) should accept an additional propwith
. - all props in
with
should be provided to the underlying component, and should take precedence over the extra props supplied to the base component. -
with
props can be indefinitely nested and provide their ownas
prop andwith
prop (assuming theas
types also comply with the as-with interface specification) . - the
ref
prop for the base component (e.g.<Button>
) should reflect the utlimate (ortail
type) component that is finally rendered.
How
By wrapping React's provided forwardRef
and it's related TypeScript types, we provide a function and the related interfaces for supporting this pattern – in a strictly typed way.
What
The forwardRefAs
function provides the functionality for this composition pattern and has roughly the interface specified:
function forwardRefAs<TDefaultComponent, TOwnProps, TForwardsProps> (
constructor: (props: TOwnProps, ref: any) => React.ReactElement<any> | null;
defaultProps: {
as: TDefaultComnponent,
};
) => React.ComponentType<TOwnProps & React.ComponentProps<TAsComponent> & React.RefAttributes<TAsComponent> ...);