Billy Bones
Billy Bones

Reputation: 2965

useRef TypeScript - not assignable to type LegacyRef<HTMLDivElement>

I am trying to use useRef with TypeScript but am having some trouble.

With my RefObject (I assume) I need to access current. (ie node.current)

I have tried the following

but when I go to set the ref I am always told that X is not assignable to type 'LegacyRef<HTMLDivElement> | undefined'.

return <div ref={ node }>{ children }</div>

Edit: this should not be restricted to any one type of element so not just HTMLDivElement | HTMLFormElement | HTMLInputElement

Edit: This should work as an example

import React, { useRef, RefObject } from 'react';

function Test() 
{
    // const node = useRef(null);
    // const node: RefObject<HTMLElement> = useRef(null);
    const node = useRef<HTMLElement | null>(null);

    if (
        node &&
        node.current &&
        node.current.contains()
    ){ console.log("current accessed")}

    return <div ref={ node }></div>
}

Upvotes: 198

Views: 172400

Answers (11)

Iv Ov
Iv Ov

Reputation: 380

Reason For The Issue

The root of the problem here is not the type of your refObject (what is returned from useRef()). The problem is that the ref prop expects a more specific type.

Div's ref says: <​div ref={..šŸ—£"Give me HTMLDivElement!"
Canvas' ref: <​canvas ref={..šŸ—£"Give me HTMLCanvasElement!"`

Props of, for example, <canvas> are like

// props of <canvas>
{
    ....
    ref: {
        current: HTMLCanvasElement
    },
};

As you can see, it expects to get HTMLCanvasElement. And if we try to pass the parent class (HTMLElement), it complains: "It won't be enough for me! It doesn't have such and such properties!"

The situation is similar to this assignment:

    // āŒ Error, of course
    const myElement: HTMLCanvasElement = new HTMLElement();

    // (Type 'HTMLElement' is missing the following properties from type 'HTMLCanvasElement': captureStream, getContext, .....)

The Absence Of A Solution

What "generalized" value can you pass to a variable if it expects to get something specific?
ā€” None.

You can't even extend its type by explicitly listing the possible options, like HTMLDivElement | HTMLFormElement | HTMLInputElement,

const generalizedValue = getElement() as `HTMLDivElement | HTMLCanvasElement | HTMLInputElement`

// āŒ Nope! What if the passed object won't have "autofocus" or "placeholder" props ?!
const myInput: HTMLInputElement = generalizedValue;

// āŒ Nope! What if the passed object won't be able to captureStream() ?!
const myCanvas: HTMLCanvasElement = generalizedValue;

Still, A Solution! (in a sense)

You wanted to make your ref universal - so it's probably created in some reusable custom hook. Just don't hesitate to specify custom generics in it!

This is where the parent class comes in handy! ā€” <T extends HTMLElement>!

const useMyCoolHook = <ElType extends HTMLElement>() => {
    const elRef = useRef<ElType>(null);

    //...your logic goes here...

    return elRef;
}

function MyButton() 
{
    const firstButtonRef = useMyCoolHook<HTMLButtonElement>(null); //<-- for <button>
    const anotherButtonRef = useMyCoolHook<HTMLButtonElement>(null); //<-- for <button>
    const linkRef = useMyCoolHook<HTMLAnchorElement>(null);   //<-- for <a>
    return (
        <>
            <button ref={ firstButtonRef } />
            <button ref={ anotherButtonRef } />
            <a ref={ linkRef } />
        </>
    );
}

If in most cases it will be used with one specific type of element (say, with <div>s), you can make it a default value of your generic: <ElType extends HTMLElement = HTMLDivElement>

const useMyCoolHook = <ElType extends HTMLElement = HTMLDivElement>() => {
    ....

Upvotes: 2

Daryl Wong
Daryl Wong

Reputation: 2443

I have the same error and there was a suggestion to convert to unknown first:

ref={inputRef as unknown as RefObject<HTMLTextAreaElement>}

Upvotes: 2

Plvtinum
Plvtinum

Reputation: 317

The reason why you're getting this error in typescript is because you need to type the ref.current and not just the ref

example:

const ref = React.useRef<HTMLDivElement>(null);

Component(props: {ref?: {current: HTMLDivElement}})

Upvotes: -1

Artem
Artem

Reputation: 117

As a workaround you can explicitly cast:

const ref = useRef<HTMLDivElement>();
...
<input
      ref={ref as RefObject<HTMLInputElement>}
      ...
/>

Upvotes: 4

Filip Malek
Filip Malek

Reputation: 169

After looking at all answers here I am not sure that any of them answer/explain the problem fully. In this case problem is two fold:

  • we need to use a different HTML interface object -> HTMLDivElement
  • we need to pass a default value to the reference initialization

React useRef function actually has 3 different signatures (in React 16.8):

If the passed default value is the same type as the generic type

function useRef\<T>(initialValue: T): MutableRefObject<T>;

If we pass a default value of the same type as the generic type OR null (as spotted by many responses here)

function useRef\<T>(initialValue: T|null): RefObject<T>;

If we pass no default values AND/OR we pass no generic type

function useRef\<T = undefined>(): MutableRefObject<T | undefined>;

The real issue comes down to incompatibility of MutableRefObject and LegacyRef. I am guessing React team did changes to the data types of references and therefor we have multiple signatures and types of references based on what we pass.

It would be nice to get a nicer error message from react eslint at least as the generic TS error message we get isn't enough to immediately spot what's wrong.

Tl;dr: passing null as the default value in useRef object will give us back the reference object that both satisfies MutableRefObject and LegacyRef (probably)

Upvotes: 2

HduSy
HduSy

Reputation: 57

selfref = React.createRef<HTMLInputElement>()

I give the exacted TS Type, then the editor passed the check. it works nice now.

Upvotes: 4

Comi9
Comi9

Reputation: 99

The same stands for the <svg> elements:

const ref = useRef<SVGSVGElement>(null)
...
<svg ref={ref} />

Upvotes: 6

Sheldonfrith
Sheldonfrith

Reputation: 3285

None of the above worked for me, but turns out the solution was quite simple...

All I was doing wrong was not explicitly including "null" as the parameter in the useRef initialization (it expects null, not undefined). Also you CANNOT use "HTMLElement" as your ref type, you have to be more specific, so for me it was "HTMLDivElement" for example).

So working code for me was something like this:

const ref = useRef<HTMLDivElement>(null);

return <div ref={ref}> Some Content... </div>

Upvotes: 271

Brady Dowling
Brady Dowling

Reputation: 5522

I came here looking for help with an iframe ref. Perhaps this solution will help someone else that's looking for the same thing. I replaced HTMLDivElement with HTMLIFrameElement so:

const node = useRef<HTMLIFrameElement>(null);

Upvotes: 5

Just import React:

import React, { useRef } from 'react';

function Test() {
    const node = useRef<HTMLDivElement>(null);

    if (
        node &&
        node.current &&
        node.current.contains()
    ){ console.log("current accessed")}

    return <div ref={node}></div>
}

I made an update. Use HTMLDivElement as generic parameter instead of HTMLElement | null. Also, contains expects an argument.

UPDATE useRef expects generic argument of DOM element type. You don't need to use | null because RefObject already knows that current might be null.

See next type:

interface RefObject<T> {
  readonly current: T | null
}

TS & React are smart enough to figure out that your ref might be null

Upvotes: 69

a2best
a2best

Reputation: 252

Key is to use HTMLElement and undefined for initialization

const node = useRef<HTMLElement>();

Upvotes: 1

Related Questions