50

I'm trying to call the child component method from the parent component using useRef.

In the future, the SayHi method will update the hook state in the child component. Unfortunately, I have bugs I can't deal with.

Line: ref.current.SayHi();

Property 'SayHi' does not exist on type 'ForwardRefExoticComponent<{ name: string; } & RefAttributes<{ SayHi: () => void; }>>'.

Line: <Child name="Adam" ref={ref}/>

Type 'RefObject<ForwardRefExoticComponent<{ name: string; } & RefAttributes<{ SayHi: () => void; }>>>' is not assignable to type '((instance: { SayHi: () => void; } | null) => void) | RefObject<{ SayHi: () => void; }> | null | undefined'. Type 'RefObject<ForwardRefExoticComponent<{ name: string; } & RefAttributes<{ SayHi: () => void; }>>>' is not assignable to type 'RefObject<{ SayHi: () => void; }>'. Property 'SayHi' is missing in type 'ForwardRefExoticComponent<{ name: string; } & RefAttributes<{ SayHi: () => void; }>>' but required in type '{ SayHi: () => void; }'.

Full test.tsx file:

import React, { useRef, forwardRef, useImperativeHandle, Ref } from 'react'

const Parent = () => {
  const ref = useRef<typeof Child>(null);
  const onButtonClick = () => {
    if (ref.current) {
      ref.current.SayHi();
    }
  };
  return (
    <div>
      <Child name="Adam" ref={ref}/>
      <button onClick={onButtonClick}>Log console</button>
    </div>
  );
}

const Child = forwardRef((props: {name: string}, ref: Ref<{SayHi: () => void}>)=> {
  const {name} = props;
  useImperativeHandle(ref, () => ({ SayHi }));
  
  function SayHi() { console.log("Hello " + name); }
  
  return <div>{name}</div>;
});

I deeply ask for help on this topic.

fredrivett
  • 5,419
  • 3
  • 35
  • 48
Adam Nowicki
  • 504
  • 1
  • 4
  • 9

3 Answers3

84

You require to extract the ref type elsewhere:

interface RefObject {
  SayHi: () => void
}

then just refer to it in both places

const Child = forwardRef((props: {name: string}, ref: Ref<RefObject>)=> {
  const {name} = props;  
  useImperativeHandle(ref, () => ({ SayHi }));
  function SayHi() { console.log("Hello " + name); }

  return <div>{name}</div>;
});
const Parent = () => {
    const ref = useRef<RefObject>(null);
    const onButtonClick = () => {
      if (ref.current) {
        ref.current.SayHi();
      }
    };
    return (
      <div>
        <Child name="Adam" ref={ref}/>
        <button onClick={onButtonClick}>Log console</button>
      </div>
    );
}
Federkun
  • 36,084
  • 8
  • 78
  • 90
5

Just replace the declaration of your ref with this const ref = useRef<{ SayHi: () => void }>(null);

giotskhada
  • 2,326
  • 1
  • 11
  • 18
0

The problem with useRef<typeof SomeForwardRefComponent> is, it thinks the ref.current will get the object type returned by forwardRef. It's not smart enough to look up the ref type. As a result, it expects properties like ref.current.displayName not ref.current.focus() etc.

You can extract the type you passed to the forwardRef's ref argument using React's ElementRef type tool, like useRef<ElementRef<typeof Child>>:

import React, { type ElementRef, type Ref, forwardRef, useRef } from 'react'

const Child = forwardRef<SomeElementType, Props>((props, ref) => (
  /* ...render something */
))
// ...or (same thing but with slightly different syntax where you specify ref type):
const Child = forwardRef((props: Props, ref: Ref<SomeElementType>) => (
  /* ...render something */
))

const Parent = () => {
  // ref here has the same type as if you did useRef<SomeElementType>
  const ref = useRef<ElementRef<typeof Child>>(null)
  return (
    <Child ref={ref} ... />
  )
}

This works, but ElementRef<typeof Child> is a bit long-winded. It's basically just a complicated alias for whatever you passed as a type parameter to forwardRef.

This may be the best solution if you're importing the forwardRefed component from a library you don't control, but if you control the code of both parent and child, you're probably better off just defining the ref type then exporting and importing it. For example:

// In `child.tsx`
import React, { forwardRef } from 'react'

export type ChildRefType = SomeElementType
export type ChildProps = { ... }

export const Child = forwardRef<ChildRefType, ChildProps>((props, ref) => (
  /* render something */
))
// In `parent.tsx`
import React, { useRef } from 'react'
import { Child, ChildRefType } from './child'

export const Parent = () => {
  // ref here has the same type as if you did useRef<SomeElementType>
  const ref = useRef<ChildRefType>(null)
  return (
    <Child ref={ref} ... />
  )
}
user56reinstatemonica8
  • 32,576
  • 21
  • 101
  • 125