179

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'.
3
  • I think HTMLInputElement is correct, but inputRef should be set to null initially, useRef<HTMLInputElement(null) Commented Sep 19, 2019 at 19:13
  • I thought so too. That works if ref is captured during React's render - <input ref={myRef} /> - not setting myRef.current = ... Commented Sep 19, 2019 at 19:30
  • 1
    This might help: github.com/DefinitelyTyped/DefinitelyTyped/issues/… specifically ref7 Commented Sep 19, 2019 at 22:35

4 Answers 4

417

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.

Sign up to request clarification or add additional context in comments.

7 Comments

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.
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.
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.
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.
@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.
|
18

as key.

You can use it like this for input component.

const inputRef = useRef<HTMLInputElement>();

2 Comments

why casting if you useRef provides generic type
you are right, there is no need for that. edited thanks.
11

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.

1 Comment

This answer is more appropriate for stackoverflow.com/questions/65638439/… where it has similar answers.
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();

1 Comment

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.