What typescript type do I use with useRef() hook when setting current manually?
Asked Answered
A

4

167

How can I use a React ref as a mutable instance, with Typescript? The current property appears to be typed as read-only.

I am using React + Typescript to develop a library that interacts with input fields that are NOT rendered by React. I want to capture a reference to the HTML element and then bind React events to it.

  const inputRef = useRef<HTMLInputElement>();
  const { elementId, handler } = props;

  // Bind change handler on mount/ unmount
  useEffect(() => {
    inputRef.current = document.getElementById(elementId);
    if (inputRef.current === null) {
      throw new Exception(`Input with ID attribute ${elementId} not found`);
    }
    handler(inputRef.current.value);

    const callback = debounce((e) => {
      eventHandler(e, handler);
    }, 200);

    inputRef.current.addEventListener('keypress', callback, true);

    return () => {
      inputRef.current.removeEventListener('keypress', callback, true);
    };
  });

It generates compiler errors: semantic error TS2540: Cannot assign to 'current' because it is a read-only property.

I also tried const inputRef = useRef<{ current: HTMLInputElement }>(); This lead to this compiler error:

Type 'HTMLElement | null' is not assignable to type '{ current: HTMLInputElement; } | undefined'.

  Type 'null' is not assignable to type '{ current: HTMLInputElement; } | undefined'.
Arrearage answered 19/9, 2019 at 18:40 Comment(3)
I think HTMLInputElement is correct, but inputRef should be set to null initially, useRef<HTMLInputElement(null)Lunn
I thought so too. That works if ref is captured during React's render - <input ref={myRef} /> - not setting myRef.current = ...Arrearage
This might help: github.com/DefinitelyTyped/DefinitelyTyped/issues/… specifically ref7Lunn
D
396

Yeah, this is a quirk of how the typings are written:

function useRef<T>(initialValue: T): MutableRefObject<T>;
function useRef<T>(initialValue: T | null): RefObject<T>;
  • If the type of the initialValue and type parameter T match, you'll hit the first override and get a MutableRefObject<T>.
  • If the type of the initialValue includes null and type parameter T doesn't, you'll hit the second override and get an immutable RefObject<T>.

You're hitting the second case when you're doing this:

useRef<HTMLInputElement>(null)

T is specified as HTMLInputElement and the type of null is inferred as HTMLInputElement | null.

You can hit the first case by doing this:

useRef<HTMLInputElement | null>(null)

T is specified as HTMLInputElement | null and the type of null is inferred as HTMLInputElement | null.

Disquisition answered 20/9, 2019 at 18:6 Comment(7)
Thank you! That makes a ton of sense and got my code to compile. I used const inputRef = useRef<HTMLElement | null>(null); to avoid a compiler error from inputRef.current = document.getElementById(elementId); Not sure if that's worth an edit.Arrearage
Yeah, if you don't need any input-specific properties from the ref, then I'd do what you did and broaden the type you annotate the ref with; if you do need input-specific properties, I'd probably do getElementById(elementId) as HTMLInputElement before assigning.Disquisition
as an fyi to anyone else I found this couldn't be done without the useRef hook, createRef wouldn't allow it. Maybe there could be a way but I couldn't find it.Thorner
Doesn't seem to work for variable (assigned in a useEffect) const shaderMat: React.RefObject<THREE.ShaderMaterial|null> = useRef(null); doesn't allow me to do shaderMat.current = mat; because curretn is read-only.Baronet
@AmbroiseRabier You've explicitly annotated shaderMat to be a RefObject, which is the immutable type, so yeah, it's not going to let you mutate it in a useEffect. You need to use the type params for useRef to control the type, not an annotation on the variable itself.Disquisition
@Disquisition Was confused on the first part of your response, I just realized you type a generic parameter on useRef function, and not the RefObject type.Baronet
I am getting the following error for you line Uncaught TypeError: Cannot read properties of undefined (reading '__H')Masefield
D
16

as key.

You can use it like this for input component.

const inputRef = useRef<HTMLInputElement>();
Donnie answered 17/7, 2022 at 16:8 Comment(2)
why casting if you useRef provides generic typeCaernarvonshire
you are right, there is no need for that. edited thanks.Donnie
P
10

I came to this question by searching how to type useRef with Typescript when used with setTimeout or setInterval. The accepted answer helped me solve that.

You can declare your timeout/interval like this

const myTimeout = useRef<ReturnType<typeof setTimeout> | null>(null)

And to clear it and set it again, you do it as usual:

const handleChange = () => {
  if (myTimeout.current) {
    clearTimeout(myTimeout.current)
  }
  myTimeout.current = setTimeout(() => {
    doSomething()
  }, 500)
}

The typing will work both if you're running in Node or in a Browser.

Panegyrize answered 2/2, 2022 at 18:19 Comment(1)
This answer is more appropriate for #65638939 where it has similar answers.Esposito
L
1

you have to write code like this:

const inputRef = useRef<HTMLInputElement>(null);

and when you need to use it you have to write it like this:

inputRef.current?.focus();
Lindholm answered 24/7, 2023 at 22:4 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Whitford

© 2022 - 2024 — McMap. All rights reserved.