Reputation: 3860
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
Reputation: 89314
To access a ref while also forwarding it:
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 refimport { 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
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
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
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
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
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
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