What's the correct way to use useRef and forwardRef together?
Asked Answered
L

3

44

I have two components: App and Child. I use useRef hook to get the DOM element in Child component and do some work with it.

I also pass ref from App component to Child component using React.forwardRef API.

App.tsx:

import { useRef } from "react";
import Child from "./Child";

export default function App() {
  const ref = useRef(null);
  console.log("App ref: ", ref);
  return (
    <div className="App">
      <Child ref={ref} />
    </div>
  );
}

Child.ts:

import React, { useEffect, useRef } from "react";

export default React.forwardRef((props, ref) => {
  const innerRef = useRef<HTMLDivElement>(null);
  const divElement = ref || innerRef; // TSC throw error here.
  useEffect(() => {
    if (divElement.current) {
      console.log("clientWidth: ", divElement.current.clientWidth);
      // do something with divElement.current
    }
  }, []);
  return <div ref={divElement}></div>;
});

I am not sure it's a correct way to use innerRef and forwarded ref together like above. At least, the TSC throw an error which means it does NOT allow use it like this.

Type '((instance: unknown) => void) | MutableRefObject' is not assignable to type 'string | ((instance: HTMLDivElement | null) => void) | RefObject | null | undefined'. Type 'MutableRefObject' is not assignable to type 'string | ((instance: HTMLDivElement | null) => void) | RefObject | null | undefined'. Type 'MutableRefObject' is not assignable to type 'RefObject'. Types of property 'current' are incompatible. Type 'unknown' is not assignable to type 'HTMLDivElement | null'. Type 'unknown' is not assignable to type 'HTMLDivElement'.ts(2322) index.d.ts(143, 9): The expected type comes from property 'ref' which is declared here on type 'DetailedHTMLProps<HTMLAttributes, HTMLDivElement>'

I want to get the correct ref in App component and use this ref in Child component as well. How can I do this?

codesandbox

Lajoie answered 28/6, 2021 at 11:32 Comment(2)
I think this is the answer you are looking for https://mcmap.net/q/335051/-property-39-current-39-does-not-exist-on-type-39-instance-htmldivelement-null-gt-void-refobject-lt-htmldivelement-gt-39Civvies
This answer explains the ref types and provides a React hook (in TypeScript) that should solve the problem.Sastruga
L
52

You can largely ignore the TS error because you didn't provide types for the forwardRef arguments so it doesn't know that ref/innerRef are equivalently typed.

const divElement = ref || innerRef; <div ref={divElement}></div>

I don't understand this bit - do you want it to have a reference to the div element directly regardless of if the is given a ref or not?

Anyway, I think if you want to get it working like this, it's best you use the useImperativeHandle hook like useImperativeHandle(ref, () => innerRef.current);

So, the Child component itself manages the ref with the innerRef but if someone does pass a ref to the Child component, it will yield that.


export default React.forwardRef((props, ref) => {
  const innerRef = useRef<HTMLDivElement>(null);

  useImperativeHandle(ref, () => innerRef.current);

  useEffect(() => {
    if (innerRef.current) {
      console.log("clientWidth: ", innerRef.current.clientWidth);
    }
  }, []);

  return <div ref={innerRef}></div>;
});

Lowkey answered 28/6, 2021 at 12:23 Comment(4)
What you can do, to prevent the typescript error is to specify the type of forwardRef like this: React.forwardRef<HTMLDivElement, yourOwnPropsType>((props, ref) => {...} and use the handle like this: useImperativeHandle(ref, () => innerRef.current as HTMLDivElement); Empressement
This should be part of the official documentation!Andorra
Nice! Or just use ! in the end: useImperativeHandle(ref, () => innerRef.current!)Contraceptive
@Contraceptive Unfortunately, can't use that non-null assertion in projects with the ESLint config that prevents that. Forbidden non-null assertion. (eslint@typescript-eslint/no-non-null-assertion)Rivalee
O
3

If you're looking for a solution with TypeScript and without useImperativeHandle, you can write this:

interface Props {
  children: React.ReactNode;
  // more...
}

export default React.forwardRef<HTMLDivElement, Props>(({ children }, ref) => {
  const innerRef = useRef<HTMLDivElement>();
   
  const setRef = (element: HTMLDivElement) => {
    if (typeof ref === "function") {
      ref(element);
    } else if (ref) {
      ref.current = element;
    }
    innerRef.current = element;
    console.log("clientWidth: ", innerRef.current.clientWidth);
  };
    
  return <div ref={setRef}>{children}</div>;
});

Demo here: https://codesandbox.io/s/beautiful-shadow-n83nmw?file=/src/App.tsx

The reason it works is that the ref prop accepts a callback, so you can execute whatever code you like when it triggers. Here, we're setting both the ref value (which we said can be a function, thus the if / else) and our innerRef.

Note: there's nothing wrong with useImperativeHandle, feel free to use it. Both solutions will perform the same.

Overbid answered 16/6, 2023 at 12:5 Comment(1)
this one is really clean though if you can make the one liner if, else-if with bracket. Sometimes it's getting confused that the next line after the else-if belongs to the else-if condition or not by first glance..Schizothymia
Z
2

To access a ref while also forwarding it:

  • Attach a ref created inside the component to the element
  • Call the useImperativeHandle hook on the outer ref (which is being forwarded to) and pass a function that returns the current property of the inner ref, which is the value that will be set to the current property of the outer ref
import { forwardRef, useImperativeHandle, useRef, useEffect } from 'react';
export default forwardRef<HTMLDivElement>((props, outerRef) => {
  const innerRef = useRef<HTMLDivElement>(null);
  useImperativeHandle(outerRef, () => innerRef.current!, []);
  // remember to list any dependencies of the function that returns the ref value, similar to useEffect
  useEffect(() => {
    if (innerRef.current) {
        // innerRef.current refers to the div element here
    }
  }, []);
  return <div ref={innerRef}></div>;
});
Zurkow answered 6/9, 2023 at 21:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.