Jimmy
Jimmy

Reputation: 3860

using ref.current in React.forwardRef

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

Upvotes: 75

Views: 48747

Answers (7)

Unmitigated
Unmitigated

Reputation: 89314

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

Upvotes: 2

Nathan
Nathan

Reputation: 3175

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

Upvotes: 45

zoran404
zoran404

Reputation: 2946

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

Upvotes: 4

Nicholas Tower
Nicholas Tower

Reputation: 85012

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

Upvotes: 102

windmaomao
windmaomao

Reputation: 7670

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.

Upvotes: 29

Justin Joy
Justin Joy

Reputation: 383

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

Upvotes: 2

Quentin Gaultier
Quentin Gaultier

Reputation: 318

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

Upvotes: 11

Related Questions