153

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'.
JPollock
  • 3,218
  • 4
  • 26
  • 36
  • I think `HTMLInputElement` is correct, but inputRef should be set to `null` initially, `useRef – dev Sep 19 '19 at 19:13
  • I thought so too. That works if ref is captured during React's render - `` - not setting `myRef.current = ...` – JPollock Sep 19 '19 at 19:30
  • 1
    This might help: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31065#issuecomment-446660394 specifically `ref7` – dev Sep 19 '19 at 22:35

4 Answers4

373

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 initial value includes null, but the specified type param doesn't, it'll be treated as an immutable RefObject.

When you do useRef<HTMLInputElement>(null), you're hitting that case, since T is specified as HTMLInputElement, and null is inferred as HTMLInputElement | null.

You can fix this by doing:

useRef<HTMLInputElement | null>(null)

Then T is HTMLInputElement | null, which matches the type of the first argument, so you hit the first override and get a mutable ref instead.

Retsam
  • 30,909
  • 11
  • 68
  • 90
  • 3
    Thank you! That makes a ton of sense and got my code to compile. I used `const inputRef = useRef(null);` to avoid a compiler error from `inputRef.current = document.getElementById(elementId);` Not sure if that's worth an edit. – JPollock Sep 20 '19 at 20:34
  • 2
    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. – Retsam Sep 20 '19 at 22:21
  • 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. – rt_ Jan 13 '20 at 21:22
  • Doesn't seem to work for variable (assigned in a useEffect) `const shaderMat: React.RefObject = useRef(null);` doesn't allow me to do `shaderMat.current = mat;` because curretn is read-only. – Ambroise Rabier Jul 09 '20 at 14:41
  • 2
    @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. – Retsam Jul 09 '20 at 17:05
  • @Retsam 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. – Ambroise Rabier Jul 09 '20 at 17:48
16

as key.

You can use it like this for input component.

const inputRef = useRef() as MutableRefObject<HTMLInputElement>;
Gucal
  • 842
  • 1
  • 11
  • 19
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.

Dharman
  • 30,962
  • 25
  • 85
  • 135
Luisus
  • 363
  • 1
  • 4
  • 8
  • This answer is more appropriate for https://stackoverflow.com/questions/65638439/type-for-useref-if-used-with-setinterval-react-typescript where it has similar answers. – Pavlo Zhukov Feb 15 '22 at 10:59
0

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();
  • 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](/help/how-to-answer). – Community Jul 27 '23 at 20:06