Yesterday, I published Introducing rbx: React, Bulma, đ â where I⊠introduced rbx (documentation), a new UI Framework for React built on top of the Bulma CSS Library. If youâve not checked it out, go ahead and do so!
One of the architectural principles was to do âno more than necessaryâ. I donât want code bloat, especially if Iâm going to be the sole maintainer of this. However, Iâd love to make it easy for others to join in the fun â so donât you lurkers get any ideas. đ
In the conclusion I noted that the core of rbx was ForwardRefAsExoticComponent
, but an eagle-eyed reader would notice that) â in the conclusion and without context â this hadnât been previously introduced. Theyâd wonder what exactly that was. Perhaps theyâd click into the code link for exotic.ts, and really not know what it is. But here, Iâll demystify the most powerful feature of rbx: the forwardRefAs
đȘ.
Iâll also use one-too-many emoji to keep this from becoming too tedious!
Why should I care about forwardRefAs
⊠whatever?
Truly, itâs an implementation detail, and youâll never be exposed to it as a user. Well, youâll be exposed to itâs benefits, but youâll never see or need to touch the function forwardRefAs
or itâs companion the TypeScript type ForwardRefAsExoticComponent
.
While I give simple credit to this in the official docs / Inversion of Control, thereâs of course more to be explained. đ€
There are two features of forwardRefAs
â which I think users will love â and in my own humble opinion, should be standard in many React packages (if not all UI Frameworks).
- Components that use it provide an
as
prop. â€ïž - Components that use it forward the
ref
prop. â€ïž
The `a
s` prop
The former (the as
prop) allows you to render any component as any other component (effectively making it a wrapper, or higher-order component). This is just syntactic sugar. đŹ
For example, if you want to render a <Button>
as an HTML button, the default, no effort is needed:
It can also use any attribute that the <button> exposes, like formTarget
or type
.
How about if you want to render it as an HTML anchor?
It can now use any attribute that the <a> exposes, like href
.
But what about a more complex example â a non-JSX.IntrinsicElement
tag â like a React-Router Link? Well, thatâs supported too.
Notice how the <Button>
component now accepts the React-Router Linkâs prop to
?
Hereâs a CodeSandbox demoing these examples:
As an alternative, the React Router Button Link could be achieved much more verbosely, but explicitly by:
Thatâs ugly, prone to typos, and ⊠we can (we did) simplify it.
The `re
f` prop
The ref
prop is the âescape-hatchâ in React đ â it gives you access to either an underlying DOM element or an underlying component. React doesnât provide this prop value to components (nor the key
prop). But, if you use the React function React.forwardRef
(read more in: Forwarding Refs â React), you actually get a factory that intercepts the ref
prop, letâs you manipulate it, and ultimately pass it on to some underlying function component.
This is incredibly useful for things like form inputs, media playback, or third party DOM libraries. All too often with React packages, a ref
goes missing when itâs needed most. Tasks like focusing on an <input>
when the component mounts become impossible. đ
But not with rbx! All components forward the ref
prop to the underlying component.
Hereâs a somewhat contrived example, where if you click the button, you can focus the text input. You can then click elsewhere to unfocus the element, and click the button to again focus the text input.
The <Generic>
component
All is good and well with these, but what about if you want to style something thatâs not an rbx component, and still take advantage of the awesome as
and ref
props? Well, thatâs what the <Generic>
component is for â and in fact, thatâs what the other components ultimately render through.
In the above example, we use <Generic as=âpâ backgroundColor=âwarningâ>
to generate an HTML paragraph with a yellow-background (hint: the backgroundColor
prop is not an attribute of the <p> element!).
Thatâs it. You can go home now. Or, if youâre still super curious, read on.
The Inner-Details and Implementation
The forwardRefAs
function and itâs corresponding ForwardRefAsExoticComponent
type have funky names. What gives? đâ
The first one is easy to answer. Itâs name is based on Reactâs forwardRef
function (and actually just wraps it). You can read more about it below.
The second one is certainly more curious. Internal to React (and its TypeScript types) there exists a type known as ForwardRefExoticComponent
which is the return value of the React.forwardRef
function (see the code on DefinitelyTyped).
The âexoticâ name implies that the value isnât really a component, itâs more of a factory that produces a component. That might sound strange, but as you can see with React.forwardRef
which has the signature:
that function takes on parameter: a RefForwardingComponent
(a function component that receives the props and returns a JSX.Element
or null
, etc.)
Hereâs a silly simple example đ»:
See how the function component is actually the parameter inside React.forwardRef
? Yeah, what we get back from React.forwardRef
. Itâs wild, itâs crazy, itâs a ForwardRefExoticComponent
. đŽ
The `forwardRefAs` function
At a high level, this function takes two parameters: the function component which renders the prop (just like react.forwardRef
did), and a second parameter that is the defaultProps
(which by the way, has a required property as
â the default React.ReactType
to ârender asâ).
Hereâs a super simple implementation of a component that uses forwardRefAs
, <Footer>
:
Basically, this component just adds âfooterâ
to the className
prop, which is then forwarded to <Generic>
â along with any of the standard HelpersProps
(props like textColor
, textSize
, etc.). It tells Generic
that it should render a div
, too (which it turns out is actually the default that <Generic>
renders as anyway).
However, due to the defaultProps
(i.e. {as: âdivâ}
), TypeScript will auto-magically type check and auto-complete props on <Footer>
that div
takes. Again, if you wanted to render it as something else (as described above), just pass it an alternative as
prop and any props that that component takes:
I donât know how youâd implement a FunkComponent
or what a funkyLevel
is, but I donât care, and neither does rbx. đ
The actual code for forwardRefAs
is:
Hopefully, thatâs not too much of an eyesore (if it is, just wait! đż). Basically, we are saying the signature is as Iâve described above â it takes a function component and defaultProps
with a requisite as
property.
In the function body is the expected usage of React.forwardRef
, where we create an âexoticâ component and set its defaultProps
. Then, we cast it as the ForwardRefAsExoticComponent
(which TypeScript ensures is appropriate; i.e. the signatures match).
the `ForwardRefAsExoticComponent`
type
And here is truly the final piece, the crux of rbx, and what probably should be the center-piece of many other packages: the ForwardRefAsExoticComponent
. Letâs dig in before our dinner gets cold. đ„©đ„Š
Letâs look at this beast. Itâs a TypeScript generic
with a call signature that optionally takes an as
prop (which is the generic type TAsComponent
).
If an as
prop is supplied, TypeScript will infer that componentâs props and use them for type-checking and auto-completion. If the as
prop is not supplied, TypeScript will use the default component supplied to forwardRefAs
(remember that defaultProps.as
?) because weâve set it as the default value of TAsComponent
. Nom nom nom. đ
It also says that it returns a JSX.Element
or null
(as all good React Components do), and guarantees that the as
âdefaultPropâ is set.
There are two additional types that we make use of here. The first one is Prefer
.
Prefer
simply takes two sets of props and merges them. If thereâs a collision, it prefers the former. For example, HTMLAttributes
s defines unselectable
to be the values âonâ | âoffâ
(something from the IE era). But Bulma provides better support for making elements unselectable with itâs is-unselectable
CSS class, and rbx implements this by taking a simple unselectable: boolean
prop. I.e. <Generic as=âpâ unselectable>You canât select me!</Generic>
Therefore, as thereâd be a collision in types (the string union âonâ | âoffâ
is incompatible with boolean
), we simply wonât copy over the latterâs prop type.
It really is quite simple:
This says: merge the type
P
(the first set of props) and the typeT
(the second set of props) - except for those thatP
already has.
The second type thatâs useful is a utility type that maps a key of JSX.IntrinsicElements
name to its specific HTMLElement
type. Take a look:
Ok, for you TypeScript younginâs this isnât as bad as it looks (I promise). đ€„
Itâs a TypeScript generic
type that determines if the provided type T
is a key in JSX.IntrinsicElements
â the keys are things like âdivâ
, âspanâ
, âtableâ
, etc. (see the code: JSX.IntrinsicElements) .
If the provided type is a key of JSX.IntrinsicElements
, then we dig into that type and infer
the underlying HTMLElement
or SVGElement
type â like HTMLDivElement
, HTMLSpanElement
or HTMLTableElement
.
Otherwise, we simply return the type as itâs a React.ComponentType
. This ensures we do things like pass the correct corresponding ref
type as supplied as
propâs type expects. This also enables inference, type checking, and auto-completion of the props on the underlying component. đČ
Conclusion
This is a really cool feature, and I donât think itâs possible to understate its usefulness. Despite being buried behind a name like ForwardRefAsExoticComponent
(which again, naturally flows from the official React name) â itâs really quite valuable. đ°
I can think of a few ways (already!) to improve it and make it even more valuable, and just might. But for now, and for the v2.x.y
releases of rbx, this will remain frozen.
I hope you have enjoyed this deep dive, and please feel welcome in commenting, sharing, or contributing to the repo on GitHub!