I can't type the ref correctly using useRef hook in typescript
Asked Answered
R

2

2

I'm trying to pass a ref from a parent component (an EditableCell) to a Child component (Input) and use its .current.focus property when the parent is clicked.

I'm using the forwardRef function using typescript but I have trouble typing it correctly

interface Props {
  // Some props type
}

const CellEditable = (props: Props) => {
  const [isEditing, setEditing] = useState<boolean>(false)
  const inputRef = useRef<HTMLInputElement>(null)

  const handleClick = () => setEditing(!isEditing)

  const opts = {
    ref: inputRef,
    value: props.value,
    onChange: props.onChange
  }

  return (
    <div onClick={handleClick}>
      {
        isEditing
        ? <InputText {...opts} />
        : <div>{props.value}</div>
      }
    </div>
  )
}
interface Props {
  // Some props type
}

type Ref = HTMLInputElement

const InputText = forwardRef((props: Props, ref: Ref) => {
  useEffect(() => {
    ref?.current?.focus()
  })
  
  return (
    <input
      type='text'
      ref={ref}
      value={props.value}
      onChange={props.onChange}
    />
  )
})

With the code above, I get the following error:

Argument of type '(props: Props, ref: Ref) => JSX.Element' is not assignable to parameter of type 'ForwardRefRenderFunction<unknown, Props>'.
  Types of parameters 'ref' and 'ref' are incompatible.
    Type '((instance: unknown) => void) | MutableRefObject<unknown> | null' is not assignable to type 'HTMLInputElement'.
      Type 'null' is not assignable to type 'HTMLInputElement'.  TS2345

If I type the forwardRed with generics, I get a different kind of error:

const InputText = forwardRef<Ref, Props>((props, ref) => { ... }
Property 'current' does not exist on type '((instance: HTMLInputElement | null) => void) | MutableRefObject<HTMLInputElement | null>'.
  Property 'current' does not exist on type '(instance: HTMLInputElement | null) => void'.  TS2339

Finally, typing the ref as any give me no error.

I am new to Typescript and I don't understand the error mentioned above.

Rabaul answered 5/2, 2021 at 8:51 Comment(3)
forwardRef<HTMLInputElement, Props>((props, ref) => {}) - Can you try this?Sym
I'm getting the second error: Property 'current' does not exist on type '((instance: HTMLInputElement | null) => void) | MutableRefObject<HTMLInputElement | null>'.Rabaul
Was this ever solved?Transcaucasia
T
0

Check out following sandbox: https://codesandbox.io/s/aged-night-3pwjs.

TypeScript warns you that your ref could be possibly null, therefore any code with ref.current should be wrapped with if statement, which will ensure that ref.current exists.

Tabor answered 5/2, 2021 at 9:20 Comment(2)
Thanks @Konstantin but I see tow difference between my code and yours. 1) My ref is passed down to the child component and I cannot find the right type for the ref props (passed via forwardRef). with any it works but not when I try to type it correctly. 2) Isn't ref?.current?.focus() already a null check? However, I tried with a regular if statement and it doesn't work eitherRabaul
@Rabaul Yes, there is a difference. For some reason ref is not obligatory attribute for InputText component, despite the fact that we correctly typed the forwardRef. I suppose, this is how forwardRef is working... Check this out: if you do not pass inputRef to InputText typescript will not complain about anything. Therefore it could be null in InputText. On the other hand, you can call inputRef in parent component, where it was created. All examples in this article medium.com/@martin_hotell/… have the same pattern.Tabor
F
0

See issue#24722, we can use the useForwardRef hook to handle below TS type error:

Property 'current' does not exist on type '((instance: HTMLInputElement | null) => void) | MutableRefObject<HTMLInputElement | null>'

import React, { ForwardedRef, forwardRef, useEffect, useRef, useState } from "react";

const useForwardRef = <T,>(ref: ForwardedRef<T>, initialValue: any = null) => {
    const targetRef = useRef<T>(initialValue);

    useEffect(() => {
        if (!ref) return;

        if (typeof ref === "function") {
            ref(targetRef.current);
        } else {
            ref.current = targetRef.current;
        }
    }, [ref]);

    return targetRef;
};

interface InputTextProps {
    value: string;
    onChange(value: string): void;
}

const InputText = forwardRef<HTMLInputElement, InputTextProps>((props, ref) => {
    const forwardRef = useForwardRef<HTMLInputElement>(ref);
    useEffect(() => {
        forwardRef.current.focus();
    });

    return <input type="text" ref={forwardRef} value={props.value} onChange={(e) => props.onChange(e.target.value)} />;
});

interface CellEditableProps {
    value: string;
    onChange(value: string): void;
}

export const CellEditable = (props: CellEditableProps) => {
    const [isEditing, setEditing] = useState<boolean>(false);
    const inputRef = useRef<HTMLInputElement>(null);

    const handleClick = () => setEditing(!isEditing);

    const opts = {
        ref: inputRef,
        value: props.value,
        onChange: props.onChange,
    };

    return <div onClick={handleClick}>{isEditing ? <InputText {...opts} /> : <div>{props.value}</div>}</div>;
};

TS Playground

Felonious answered 25/7 at 7:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.