using ref.current in React.forwardRef

Viewed 13166

Codesandbox here

I am trying to use a ref from a parent component to listen to certain ref events in the child component where the ref is attached to the child component using React.forwardRef. However, I am getting a linting complaint in my child component when I reference ref.current, stating:

Property 'current' does not exist on type 'Ref'. Property 'current' does not exist on type '(instance: HTMLDivElement) => void'

How am I supposed to reference a ref in a React.forwardRef component? Thanks.

index.tsx:

import * as React from "react";
import ReactDOM from "react-dom";

const Component = React.forwardRef<HTMLDivElement>((props, ref) => {
  React.useEffect(() => {
    const node = ref.current;
    const listen = (): void => console.log("foo");

    if (node) {
      node.addEventListener("mouseover", listen);
    }
    return () => {
      node.removeEventListener("mouseover", listen);
    };
  }, [ref]);

  return <div ref={ref}>Hello World</div>;
});

export default Component;

const App: React.FC = () => {
  const sampleRef = React.useRef<HTMLDivElement>(null);

  return <Component ref={sampleRef} />;
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
4 Answers

Refs are not necessarily objects with a current property. They can also be functions. So the type error is pointing out that you might be passed one of the latter. You'll need to write your code so that it can work with both variations.

This can be a bit tricky, but it's doable. Our effect can't piggy back on the function that was passed in, since that function could be doing literally anything, and wasn't written with our useEffect in mind. So we'll need to create our own ref, which i'll call myRef.

At this point there are now two refs: the one passed in, and the local one we made. To populate both of them, we'll need to use the function form of refs ourselves, and in that function we can assign the div element to both refs:

const Component = React.forwardRef<HTMLDivElement>((props, ref) => {
  const myRef = useRef<HTMLDivElement | null>(null);
  React.useEffect(() => {
    const node = myRef.current;
    const listen = (): void => console.log("foo");

    if (node) {
      node.addEventListener("mouseover", listen);
    }
    return () => {
      node.removeEventListener("mouseover", listen);
    };
  }, [ref]);

  return (
    <div ref={(node) => {
      myRef.current = node;
      if (typeof ref === 'function') {
        ref(node);
      } else if (ref) {
        ref.current = node;
      }
    }}>Hello World</div>
  );
});

elaborating on @Nicholas answer :

import React, { MutableRefObject, Ref, useEffect } from "react";
import { TextInput } from "react-native";

type CustomTextInputProps = {};

export const CustomTextInput = React.forwardRef<
  TextInput,
  CustomTextInputProps
>((props, ref) => {
  const localRef = React.useRef<TextInput | null>(null);

  useEffect(() => {
    // using the local ref
    localRef.current?.focus();
  }, []);

  return <TextInput {...props} ref={assignRefs(localRef, ref)} />;
});

const assignRefs = <T extends unknown>(...refs: Ref<T | null>[]) => {
  return (node: T | null) => {
    refs.forEach((r) => {
      if (typeof r === "function") {
        r(node);
      } else if (r) {
        (r as MutableRefObject<T | null>).current = node;
      }
    });
  };
};

We can also do something like this. First create a utility like this:

function useCombinedRefs(...refs) {
  const targetRef = React.useRef()

  React.useEffect(() => {
    refs.forEach(ref => {
      if (!ref) return
      if (typeof ref === 'function') ref(targetRef.current)
      else ref.current = targetRef.current
    })
  }, [refs])

  return targetRef
}

And use it like so:

const CustomInput = React.forwardRef((props, ref) => {
    const innerRef = React.useRef(null)
    const combinedRef = useCombinedRefs(ref, innerRef)

    return (
      <input ref={combinedRef} />
    )
})

Source and more info: Reusing the ref from forwardRef with React hooks

I searched around, since this is a good candidate for another hook such as useForwardRef. Here's the proposal, https://github.com/facebook/react/issues/24722

I also tried it myself, works perfectly for this purpose.

const InputField = React.forwardRef<HTMLInputElement, InputFieldProps>(
  (props, ref) => {
   const inputRef = useForwardRef<HTMLInputElement>(ref);
   const onLabelClick = () => {
    inputRef.current?.focus();
  };

  return <input ref={inputRef} />
 );

Of course, this is essentially the same code in the initial answer, but written as a hook.

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;
};

Note: the name of the hook is debatable, it could be named as useCopyRef, useBorrowRef whatever. Here for simplicity, because it was created for the purpose of forwardRef, we named it as useForwardRef, but actually it has nothing to do with forward.

Related