Skip to content

Instantly share code, notes, and snippets.

@dfee

dfee/blog.md Secret

Created January 23, 2019 21:46
Show Gist options
  • Save dfee/b3e4cb8257f1c10dfb3e57a56c2d8d86 to your computer and use it in GitHub Desktop.
Save dfee/b3e4cb8257f1c10dfb3e57a56c2d8d86 to your computer and use it in GitHub Desktop.

Demystifying the Backbone of 👟 rbx: ForwardRefAsExoticComponent

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.

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.

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 benefits 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 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: https://gist.github.com/de5bc743a9231863c37ed62241fc675f

It can also use any attribute that the exposes, like formTarget or type.

How about if you want to render it as an HTML anchor? https://gist.github.com/62456334a6464b29ec8579a0a607c8d4

It can now use any attribute that the 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.

https://gist.github.com/4f4f91ebe7f7c07be708acaf83d1a68b

Notice how the <Button> component now accepts the React-Router Link’s prop to?

Here’s a CodeSandbox demoing these examples: https://codesandbox.io/s/k3mzjkl6w5?autoresize=1&fontsize=12

As an alternative, this could be achieved explicitly: https://gist.github.com/bde8bf086f1977922949b2875d3e7f24

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.

rbx “ref” example - CodeSandbox

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 a member of the

element!).

That’s it. You can go home now. Or, if you’re still super curious, read on.

The 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 it’s TypeScript types) there exists a type known as ForwardRefExoticComponent which is the return value of the 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: https://gist.github.com/da1eef2a89e040c29629e33fae7b0fd8

What’s really going on is that it takes a RefForwardingComponent (a function component that receives the props and returns a JSX.Element or null, etc.

Here’s a silly simple example: https://gist.github.com/46bd2de7f94aaede483018b76213cc77

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 JSX.Element or React.ComponentType to “render as”).

Here’s a super simple implementation of a component that uses forwardRefAs, <Footer>:

https://gist.github.com/1bd442addca808246a2eafc8ed5e6e08

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 automagically 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:

https://gist.github.com/4e2b5b7f98f84891ce503501320dbeca

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: https://gist.github.com/c838e5c18c6ca10741351d1eb2c3e4b4

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 crux of many other packages: the ForwardRefAsExoticComponent. Let’s dig in before our dinner gets cold.

https://gist.github.com/7d8207b542eb1eb56901e8299b6fd121

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: https://gist.github.com/a0b4f0413589c2cabb45eba3ee234c18

“Copy the type P (the first set of props) and all the props in 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:

https://gist.github.com/618717beb602574f1e03efa96b6abbdd

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 two 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 forwardRefAs 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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment