28

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.

1
  • Could you wrap the hoverRef in a dragRef component and forward the hoverRef to a child element? Commented Feb 17, 2020 at 21:37

5 Answers 5

42

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

Sign up to request clarification or add additional context in comments.

9 Comments

Thanks. The simple solution is what I needed. A TypeScript variation ref={((el: HTMLDivElement) => setNodeRef(el)) && ref}
@MiguelJara The TypeScript part is the el: HTMLDivElement the && is a logical and shortcut. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Uncaught TypeError: myRef is not a function. Use myRef.current = el instead.
@holem current is a read-only property.
What works great? nothing in this answer or comments work. Is this an old React version?
|
22

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

2 Comments

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.
Typescript doesn't like it. Cannot assign to 'current' because it is a read-only property.ts(2540)
21

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

2 Comments

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.
Incredibly helpful answer and amazing solution. Hard to believe it works so well!
2

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>

Comments

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

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.