React Refs with TypeScript
🎒 this article uses following library versions:
{
"@types/react": "16.4.7",
"@types/react-dom": "16.0.6",
"typescript": "3.0.1",
"react": "16.4.2",
"react-dom": "16.4.2"
}
Recently I’ve got this message on Twitter
Instead of replying, I’ve decided to write down this “short post” about how to handle React DOM refs and ref forwarding with TypeScript for anyone new to React and TypeScript as I didn’t found this resource anywhere online.
Disclaimer:
Don’t expect to learn all the why’s and how’s about React refs within this blogpost ( you can find all that info in excellent React docs).
This post will try to mirror those docs a bit for easy context switching.
What are Refs in React ?
Refs provide a way to access DOM nodes or React elements created in the render method.
Let’s create some React refs with TypeScript 👌👀
Creating Refs
class MyComponent extends Component {
// we create ref on component instance
private myRef = createRef()
render() {
return <div ref={this.myRef} />
}
}
But with that component definition, we get compile error:
[ts]
Type 'RefObject<{}>' is not assignable to type 'RefObject<HTMLDivElement>'.
Uh oh? What’s going on here ? TypeScript has very good type inference especially within JSX, and because we are using ref
on an <div>
element, it knows that the ref needs to be a type of HTMLDivElement
. So how to fix this error?
React.createRef()
is an generic function
// react.d.ts
function createRef<T>(): RefObject<T>
What about the return type, the RefObject<T>
? Well that's just a Maybe
type with following interface
// react.d.ts
interface RefObject<T> {
// immutable
readonly current: T | null
}
With that covered, you already know how to make our code valid right ? 👀 We need to explicitly set the generic value for createRef
:
Accessing refs
When a ref is passed to an element in
render
, a reference to the node becomes accessible at thecurrent
attribute of the ref.
If we wanna access our previously defined ref value, all we need to do is to get the current
value from the ref object
const node = this.myRef.current
Although, our node
variable is gonna be a Maybe type 👉 HTMLDivElement
OR null
( remember? RefObject<T>
interface... ).
So if we would like to execute some imperative manipulation with that node
we just couldn't do:
class MyComponent extends Component {
private myRef = React.createRef<HTMLDivElement>() focus() {
const node = this.myRef.current
node.focus()
}
}
with that code, we would get an compile error
[ts] Object is possibly 'null'.
const node: HTMLDivElement | null
You may think right now: “this is annoying, TypeScript sucks…” Not so fast partner 😎! TypeScripts just prevents you to do a programmatic mistake here which would lead to runtime error, even without reading a line of the docs ❤️.
What React docs say about current
value ?
React will assign the current property with the DOM element when the component mounts, and assign it back to null when it unmounts. ref updates happen before componentDidMount or componentDidUpdate lifecycle hooks.
That’s exactly what TS told you by that compile error! So to fix this, you need to add some safety net, an if
statement is very appropriate here ( it will prevent runtime errors and also narrow type definition by removing null
):
Also we get autocomplete to whole HTMLDivElement
DOM api. Lovely!
Curious reader may ask:
What about accessing refs within
componentDidMount
if we don't wanna encapsulate our imperative logic within a method ( because we are messy/bad programmers 😇 ) ?
I hear you… Because we know that our refs current
value is definitely gonna be available within componentDidMount
, we can use TypeScript's Non-null assertion operator 👉 👉 👉 !
That’s it!
But hey, I really recommend encapsulating the logic to separate method with descriptive method name. Ya know readable code without comments and stuff 🖖 …
Adding a Ref to a Class Component
If we wanted to wrap our MyComponent above to simulate it being focused immediately after mounting, we could use a ref to get access to the MyComponent instance and call its focus
method manually:
import React, { createRef, Component } from 'react'class AutoFocusTextInput extends Component {
// create ref with explicit generic parameter
// this time instance of MyComponent
private myCmp = createRef<MyComponent>() componentDidMount() {
// @FIXME
// non null assertion used, extract this logic to method!
this.textInput.current!.focus()
} render() {
return <MyComponent ref={this.textInput} />
}
}
Note I: this only works if
MyComponent
is declared as a classNote II: we get access to all instance methods as well + top notch DX thanks to TypeScript
Beautiful isn’t it ? 🔥
Refs and Functional Components
You may not use the ref attribute on functional components because they don’t have instances
You can, however, use the ref attribute inside a functional component as long as you refer to a DOM element or a class component:
Forwarding Refs
Forwarding refs to DOM components
Let’s define FancyButton
component that renders the native button DOM element:
Ref forwarding is an opt-in feature that lets some components take a ref they receive, and pass it further down (in other words, “forward” it) to a child.
Let’s add ref forwarding support to this component, so components using it, can get a ref to the underlying button DOM node and access it if necessary — just like if they used a DOM button directly.
What’s happening here?
- we create and export
Ref
type for consumers of ourFancyButton
- we use
forwardRef
to obtain the ref passed to it, and then forward it to the DOM button that it renders. Again this function is a generic function which consists of 2 generic arguments:
// react.d.ts
function forwardRef<T, P = {}>(
Component: RefForwardingComponent<T, P>
): ComponentType<P & ClassAttributes<T>>
Where:
T
is type of our DOM elementP
is type of our props- return type is the final component definition with proper props and ref types
ComponentType<P & ClassAttributes<T>>
Now we can use it type-safe way:
Forwarding refs in higher-order components
Forwarding refs via HoC is quite tricky as can be seen in React docs.
TL;DR: you need to explicitly return a wrapped render of your HoC via forwardRef
API
return forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />
})
Now question is, how to handle this pattern with TypeScript…
Let’s cover this shall we ?
Here is our FancyButton
TypeScript implementation:
And this is how we wanna use it within our App:
And finally let’s implement withPropsLogger
HoC with ref forwarding support.
withPropsLogger implementation + documentation
With our forwardRef aware HoC implemented, we need to pass explicitly generic arguments to withPropsLogger
const EnhancedFancyButton = withPropsLogger<
FancyButton,
FancyButtonProps
>(FancyButton)
And finally we can use it in type-safe way! Let’s check it out:
And we’re at the end. Now go, TypeScript all the React refs things! 💪 💪 ⚛️️️️️️️
️As always, don’t hesitate to ping me if you have any questions here or on Twitter (my handle @martin_hotell) and besides that, happy type checking folks and ‘till next time! Cheers! 🖖 🌊 🏄