Using multiple refs on a single React element
Asked Answered
A

5

25

I'm using the useHover() react hook defined in this recipe. The hook returns a ref and a boolean indicating whether the user is currently hovering over element identified by this ref. It can be used like this...

function App() {
  const [hoverRef, isHovered] = useHover();

  return (
    <div ref={hoverRef}>
      {isHovered ? 'Hovering' : 'Not Hovering'}
    </div>
  );
}

Now let's say that I want to use another (hypothetical) hook called useDrag which returns a ref and a boolean indicating whether the user is dragging the current element around the page. I want to use this on the same element as before like this...

function App() {
  const [hoverRef, isHovered] = useHover();
  const [dragRef, isDragging] = useDrag();

  return (
    <div ref={[hoverRef, dragRef]}>
      {isHovered ? 'Hovering' : 'Not Hovering'}
      {isDragging ? 'Dragging' : 'Not Dragging'}
    </div>
  );
}

This won't work because the ref prop can only accept a single reference object, not a list like in the example above.

How can I approach this problem so I can use multiple hooks like this on the same element? I found a package that looks like it might be what I'm looking for, but I'm not sure if I'm missing something.

Antichrist answered 17/2, 2020 at 21:32 Comment(1)
Could you wrap the hoverRef in a dragRef component and forward the hoverRef to a child element?Decameter
D
24

A React ref is really nothing but a container for some mutable data, stored as the current property. See the React docs for more details.

{
  current: ... // my ref content
}

Considering this, you should be able to sort this out by hand:

function App() {
  const myRef = useRef(null);

  const [hoverRef, isHovered] = useHover();
  const [dragRef, isDragging] = useDrag();

  useEffect(function() {
    hoverRef.current = myRef.current;
    dragRef.current = myRef.current;
  }, [myRef.current]);

  return (
    <div ref={myRef}>
      {isHovered ? 'Hovering' : 'Not Hovering'}
      {isDragging ? 'Dragging' : 'Not Dragging'}
    </div>
  );
}
Diley answered 17/2, 2020 at 21:39 Comment(2)
Correct idea, but assigning to a ref won't cause a render, so useEffect will not get called properly. The assignment should happen in the callback instead.Seiber
Typescript doesn't like it. Cannot assign to 'current' because it is a read-only property.ts(2540)Autodidact
T
34

A simple way to go about this is documented below.

Note: the ref attribute on elements takes a function and this function is later called with the element or node when available.

function App() {
  const myRef = useRef(null);

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

Hence, myRef above is a function with definition

function(element){
 // something done here
}

So a simple solution is like below

function App() {
  const myRef = useRef(null);
  const anotherRef = useRef(null);

  return (
    <div ref={(el)=> {myRef(el); anotherRef(el);}}>
    </div>
  );
}

Note: Check out the react docs on ref-callbacks. Thanks

Tachyphylaxis answered 6/6, 2022 at 15:12 Comment(9)
Thanks. The simple solution is what I needed. A TypeScript variation ref={((el: HTMLDivElement) => setNodeRef(el)) && ref}Jovitta
Thank you so much for your answer @Hillsie, could I ask some link to some TypeScript documentation to understand that syntax? I've tried to understand it, specially the "&&", but I can't get my head around it.Hopping
@MiguelJara The TypeScript part is the el: HTMLDivElement the && is a logical and shortcut. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…Jovitta
Uncaught TypeError: myRef is not a function. Use myRef.current = el instead.Cambridgeshire
@Cambridgeshire current is a read-only property.Scapula
It is read-only because you are likely call const myRef = useRef<HTMLDivElement>(null) instead of const myRef = useRef<HTMLDivElement>(). First returns RefObject<T> which is not mutable, second returns MutableRefObject<T | undefined> that allows you to mutate the value by using myRef.current = el. You can check the source code.Prindle
What works great? nothing in this answer or comments work. Is this an old React version?Engross
I don't think this is a valid answer anymore. It may apply to a previous version. When I try this in Typescript, I get an error saying that the ref is not callable.Skiba
Anyone still looking for how this is done should read the react docs on ref-callback. Here is a link. react.dev/reference/react-dom/components/common#ref-callbackTachyphylaxis
D
24

A React ref is really nothing but a container for some mutable data, stored as the current property. See the React docs for more details.

{
  current: ... // my ref content
}

Considering this, you should be able to sort this out by hand:

function App() {
  const myRef = useRef(null);

  const [hoverRef, isHovered] = useHover();
  const [dragRef, isDragging] = useDrag();

  useEffect(function() {
    hoverRef.current = myRef.current;
    dragRef.current = myRef.current;
  }, [myRef.current]);

  return (
    <div ref={myRef}>
      {isHovered ? 'Hovering' : 'Not Hovering'}
      {isDragging ? 'Dragging' : 'Not Dragging'}
    </div>
  );
}
Diley answered 17/2, 2020 at 21:39 Comment(2)
Correct idea, but assigning to a ref won't cause a render, so useEffect will not get called properly. The assignment should happen in the callback instead.Seiber
Typescript doesn't like it. Cannot assign to 'current' because it is a read-only property.ts(2540)Autodidact
P
16

I use TypeScript and it doesn't let easily assign .current of refs created with useRef or forwarded ones (because it can be a RefCallback also). So I made a function that smartly merges all given refs into one RefCallback for clean/simple use. Check this out. Works great for me.

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

type MutableRefList<T> = Array<RefCallback<T> | MutableRefObject<T> | undefined | null>;

export function mergeRefs<T>(...refs: MutableRefList<T>): RefCallback<T> {
  return (val: T) => {
    setRef(val, ...refs);
  };
}

export function setRef<T>(val: T, ...refs: MutableRefList<T>): void {
  refs.forEach((ref) => {
    if (typeof ref === 'function') {
      ref(val);
    } else if (ref != null) {
      ref.current = val;
    }
  });
}

usage example

interface Props {
  // I pass it as a prop, you may use forwardRef - also supported by mergeRefs
  inputRef?: Ref<HTMLInputElement>;
}

export function Input({
  inputRef,
}: Props): ReactElement {
  // some local ref
  const privateRef = useRef<HTMLInputElement>(null);

  return (
    <input
      type="text"
      ref={mergeRefs(inputRef, privateRef)}
      // ... your other stuff
    />
  );
}
Pardew answered 9/6, 2023 at 14:9 Comment(2)
This didn't work with the ref-like value for useAutoAnimate from formkit. The type of its ref is (instance: Element | null) => void. It ended up causing an infinite loop in setRef above. The accepted answer worked though.Longlegged
Incredibly helpful answer and amazing solution. Hard to believe it works so well!Corley
A
0

Follwing what @Doc-Han is saying, if you ever need a reference AND also a form register for an input you can do something like this:

function App() {
  const myRef = useRef(null);
  const { register } = useForm({ defaultValues });

  const setRefs = useCallback((element) => {
    myRef.current = element;
    register({ required: 'First name is required.' })(element);
  }, [myRef, register]);

  return (
    <input ref={setRefs} />
  );
}
Approximal answered 9/3, 2023 at 16:9 Comment(0)
A
0

Apparently all the answers here are not correct, but actually they just need a little tweak:

const firstRef = useRef(null);
const secondRef = useRef(null);

<div ref={(node) => {
  fisrtRef.current = node;
  secondRef.current = node;
}}></div>
Advocation answered 20/6 at 23:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.