Demystifying the Backbone of 👟 rbx

A whimsical discussion about `forwardRefAs`

Devin Fee
7 min readJan 23, 2019
an exotic beach for an exotic discussion 🌮

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).

  1. Components that use it provide an as prop. ❀
  2. Components that use it forward the ref prop. ❀

The `as` 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 `ref` 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, HTMLAttributess 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 type T (the second set of props) - except for those that P 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!

--

--