How to use React useRef hook with typescript?
Asked Answered
P

2

62

I am creating a reference using the new useRef hook

const anchorEl = React.useRef<HTMLDivElement>(null)

And using like

<div style={{ width: "15%", ...flexer, justifyContent: "flex-end" }}>
    <Popover
        id="simple-popper"
        open={open}
        anchorEl={anchorEl}
        onClose={() => {
          setOpen(false)
        }}
        anchorOrigin={{
          vertical: 'bottom',
          horizontal: 'center',
        }}
        transformOrigin={{
          vertical: 'top',
          horizontal: 'center',
        }}
    >
        <Typography>The content of the Popover.</Typography>
    </Popover>
</div>
<div ref={anchorEl} >
      ...

but I get this error

TS2322: Type 'MutableRefObject<HTMLDivElement>' is not assignable to type 'HTMLElement | ((element: HTMLElement) => HTMLElement)'.
  Type 'MutableRefObject<HTMLDivElement>' is not assignable to type '(element: HTMLElement) => HTMLElement'.
    Type 'MutableRefObject<HTMLDivElement>' provides no match for the signature '(element: HTMLElement): HTMLElement'.
Version: typescript 3.2.2, tslint 5.12.0
Posen answered 29/1, 2019 at 12:11 Comment(0)
A
33

anchorEl variable is ref object, an object that has only current property. It's unknown how Popover works but, but it expects an element as anchorEl prop, not a ref.

It should be:

<Popover
    id="simple-popper"
    open={open}
    anchorEl={anchorEl.current}

If <Popover and <div ref={anchorEl} > are siblings like it's shown, a ref won't be ready to use at the moment when it's passed as a prop. In this case a component needs to be re-rendered on mount:

const [, forceUpdate] = useState(null);

useEffect(() => {
  forceUpdate({});
}, []);

...

   { anchorEl.current && <Popover
        id="simple-popper"
        open={open}
        anchorEl={anchorEl.current}
        ...
   }
   <div ref={anchorEl} >

In case <div ref={anchorEl} > doesn't have to be rendered to DOM, it could be

   <Popover
        id="simple-popper"
        open={open}
        anchorEl={<div/>}

The necessity to render a component twice and use forceUpdate workaround suggests that this could be done in a better way. The actual problem here is that Popover accepts an element as a prop, while accepting refs is common in React.

At this point ref object has no benefits. Ref callback can be used with useState instead, state update function is callback that receives new state as an argument and it doesn't cause additional updates if it receives the same state (DOM element):

const [anchorEl, setAnchorEl] = useState<HTMLDivElement>(null);

...

   { anchorEl && <Popover
        id="simple-popper"
        open={open}
        anchorEl={anchorEl}
        ...
   }
   <div ref={setAnchorEl} >
Abraham answered 29/1, 2019 at 12:28 Comment(10)
Great answer. In TypeScript, you'd have to pass a value to forceUpdate. I used null.Karat
Do you have to write ugly react in order to overcome a typescript compilation issue??Telemachus
@ThanasisIoannidis Can you clarify? The question had compilation error because there was a mistake in code. A fix was suggested. It would be the same for JS.Abraham
@EstusFlask I think it could be done better using anchorEl.current as a dependency in useEffect, and set a state to the value of anchorEl.current from within this useEffect. That would trigger a rerender implicity without having to force it. My point is that it could look less of a hackTelemachus
@ThanasisIoannidis Do you mean something useEffect(() => { forceUpdate(anchorEl.current) }, [anchorEl.current])? Doesn't seem much better to me. Could be done in more simple way with useState alone, updated the post. Any way, this isn't specific to TS, compilation error just shown the issue that already was there.Abraham
honestly, i dont think this is an acceptable answer. while this might get the job done this will cause problems in big projects.Traditor
@Traditor What problems exactly? I don't see how this isn't an acceptable answer. The question is about how to use a ref, and that's how. The last snippet is the way it's supposed to be done. I could agree that putting div container next to Popover may be impractical, but this wasn't discussed.Abraham
setting empty state just because you need to re-render is bad programming.Traditor
@Traditor There is an in-built type in React for useRef return value. See https://mcmap.net/q/321343/-how-to-use-react-useref-hook-with-typescriptSchmooze
"setting empty state just because you need to re-render is bad programming." Especially because you do not need it as react is already re-rendering if you use refs (pass refs to html vdom elems).Moniquemonism
S
6

You can use MutableRefObject type from 'react'

import { MutableRefObject } from "react"

const anchorEl: MutableRefObject<HTMLDivElement> = React.useRef(null);

Another way to enforce types for refs would be to use ElementRef. [1]

import { useRef, ElementRef } from "react";
 
const Component = () => {
  const audioRef = useRef<ElementRef<"audio">>(null);
 
  return <audio ref={audioRef}>Hello</audio>;
};

[1] https://www.totaltypescript.com/strongly-type-useref-with-elementref

Schmooze answered 2/12, 2022 at 5:38 Comment(5)
Can you explain why it works here?Cattegat
Can I know what did you mean by asking to explain? It's just the type for ref elements defined by React. There's nothing more to explain here I guess :).Schmooze
that's ("the type for ref elements defined by React") what I asked haha thanksCattegat
Why is this type not returned by default?Biolysis
"Why is this type not returned by default?" It is, const anchorEl = React.useRef<HTMLDivElement>(); is the same (although it contains the type undefined | HTMLDivElement instead of null | HTMLDivElement from the example)Moniquemonism

© 2022 - 2024 — McMap. All rights reserved.