React Refs with TypeScript

Martin Hochel
6 min readAug 7, 2018

--

🎒 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"
}

🎮 source code can be found on my github profile

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:

createRef with TypeScript

Accessing refs

When a ref is passed to an element in render, a reference to the node becomes accessible at the currentattribute 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 ):

type narrowing with runtime guard

Also we get autocomplete to whole HTMLDivElement DOM api. Lovely!

type narrowing and intellisense in action

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 👉 👉 👉 !

using ! operator for ref type narrowing ( I know what I’m doing trust me 👀 )

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 class

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

Refs and Functional Components

Forwarding Refs

Ref forwarding is a technique for automatically passing a ref through a component to one of its children.

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.

FancyButton with refs forwarding to DOM components

What’s happening here?

  1. we create and export Ref type for consumers of our FancyButton
  2. 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 element
  • P 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:

Fancy button with refs

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! 🖖 🌊 🏄

--

--

Martin Hochel

Principal Engineer | Google Dev Expert/Microsoft MVP | @ngPartyCz founder | Speaker | Trainer. I 🛹, 🏄‍♂️, 🏂, wake, ⚾️& #oss #js #ts