77

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

8 Answers 8

107

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>
  );
});
Sign up to request clarification or add additional context in comments.

5 Comments

Great explanation. I finally get it. But man this is brutal for a very common use case in React. I can't help but wonder if we should be adding code like this just to hack around our janky type system
Question: if we skip the instance case (if (typeof ref === "function") return; somewhere), what are the drawbacks? In some trivial tests, I always get a MutableRefObject.
The drawback is that if someone passes in a function ref, you won't call it and so it cannot do its assignment (or whatever else its code is doing). If you're writing code for yourself and you know for a fact that you will never pass in a function ref, then you won't notice any issues
amazing explanation although, it should not be that hard in React
This is not a good answer. Most of the time, doing what is recommended in this solution is a waste of time and clutters the code. If you know in advanced that your ref will only need the object form that has current, there is no reason to write any code to support some function version that will never be used.
54

All the answers in this thread are doing way too much. You can just use the useImperativeHandle hook to combine the refs.

type Props = { id: string };

export const Component = forwardRef<HTMLInputElement, Props>((props, forwardedRef) => {
  const ref = useRef<HTMLInputElement>(null);

  useImperativeHandle(forwardedRef, () => ref.current as HTMLInputElement);

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

Component.displayName = 'Component';

5 Comments

Can you please help me go around the typescript error this causes? Type 'HTMLInputElement | null' is not assignable to type 'HTMLInputElement'. Type 'null' is not assignable to type 'HTMLInputElement'.ts(2322) index.d.ts(1106, 79): The expected type comes from the return type of this signature.
@eliastouil updated my code to include an assertion that fixes your error
The best solution in my opinion, bravo!
Not only this approach is cleaner, but the official way to do it. useImperativeHandle
Also, instead of using type assertion for ref.current, you can use non-null assertion, eg. ref.current!.
29

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.

2 Comments

This is not going to work if you have conditional logic in your render function, since your useEffect will not be triggered. You can fix this by removing dependencies from useEffect which will force it to execute on every render
Great solution, but it's only kicking the can down the road. It merely a bandaid for a more fundamental flaw with React
11

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

Comments

4

The simplest way is to create a ref callback that assigns the value to both of the refs.

The best thing about this method is that it follows the way React normally assigns refs.

Here's my utility function for it:

import type {MutableRefObject, RefCallback} from 'react';

type RefType<T> = MutableRefObject<T> | RefCallback<T> | null;

export const shareRef = <T>(refA: RefType<T>, refB: RefType<T>): RefCallback<T> => instance =>
{
    if (typeof refA === 'function')
    {
        refA(instance);
    }
    else if (refA)
    {
        refA.current = instance;
    }
    if (typeof refB === 'function')
    {
        refB(instance);
    }
    else if (refB)
    {
        refB.current = instance;
    }
};

Vanilla JavaScript:

export const shareRef = (refA, refB) => instance =>
{
    if (typeof refA === 'function')
    {
        refA(instance);
    }
    else if (refA)
    {
        refA.current = instance;
    }
    if (typeof refB === 'function')
    {
        refB(instance);
    }
    else if (refB)
    {
        refB.current = instance;
    }
};

All you have to do is use this utility function when assigning refs to components:

const MyComponent = forwardRef(function MyComponent(props, forwardedRef)
{
    const localRef = useRef();

    return (
        <div ref={shareRef(localRef, forwardedRef)}/>
    );
});

I'm fairly sure you'll have at most 2 different refs, but just in case I'll leave this version that supports any number of refs:

import type {MutableRefObject, RefCallback} from 'react';

type RefType<T> = MutableRefObject<T> | RefCallback<T> | null;

export const shareRef = <T>(...refs: RefType<T>[]): RefCallback<T> => instance =>
{
    for (const ref of refs)
    {
        if (typeof ref === 'function')
        {
            ref(instance);
        }
        else if (ref)
        {
            ref.current = instance;
        }
    }
};

One thing to note here is that this callback is called on every render, which also means that if forwarded ref is a function it will also be called. This should not impact performance in any way, unless the forwarded ref function has some big performance overhead, which is something that rarely happens.

If you want to avoid the ref callback being called unnecessarily you can memorize it using useCallback:

import {MutableRefObject, RefCallback, useCallback} from 'react';

type RefType<T> = MutableRefObject<T> | RefCallback<T> | null;

export const useSharedRef = <T>(refA: RefType<T>, refB: RefType<T>): RefCallback<T> => useCallback(instance =>
{
    if (typeof refA === 'function')
    {
        refA(instance);
    }
    else if (refA)
    {
        refA.current = instance;
    }
    if (typeof refB === 'function')
    {
        refB(instance);
    }
    else if (refB)
    {
        refB.current = instance;
    }
}, [refA, refB]);

Since this involves hooks the usage is a bit different:

const MyComponent = forwardRef(function MyComponent(props, forwardedRef)
{
    const localRef = useRef();

    const sharedRef = useSharedRef(localRef, forwardedRef);

    return (
        <div ref={sharedRef}/>
    );
});

Comments

2

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

5 Comments

This is not going to work if you have conditional logic in your render function, since your useEffect will not be triggered. You can fix this by removing dependencies from useEffect which will force it to execute on every render
@zoran404, that's not true. He doesn't have conditional hook logic. We can have any conditional logic as long as it doesn't involve hook definition.
@windmaomao Maybe I was not clear. I meant to say that the developer may have conditional logic in the jsx part of the render function. If you sometimes render your jsx component and sometimes not then the ref will change, but useEffect will not detect this change, so it will not update the forwarded ref.
For example if you had something like this: return condition ? <input ref={combinedRef}/> : null the combinedRef will not be updated when the condition changes
In theory, ref will get updated when you dismount a component. But technically the pointer that gets changed is ref.current. So monitoring the ref itself might not be enough. In short, ref does get changed during mount/dismount. This is by design.
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 } from 'react';
const Component = 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

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

Comments

0

Create the ref outside of your component using useRef, then pass it down as a prop.

Comments

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.