DEV Community

Anton Korzunov
Anton Korzunov

Posted on • Updated on

The same useRef, but it will callback 🤙

For a long while we there were no refs - we had only ref, which was callback based. Something will set a ref by calling it.

class Example extends React.Component {
  state = {
    ref1: null,
  }

  ref2 = null;

  // updating ref1 would trigger update for this component
  setRef1 = (ref) => this.setState(ref1);
  // updating ref2 would just set it 
  setRef2 = (ref) => this.ref2 = ref; 

  render() {
    return <div ref={ref1}><span ref={ref2}>🤷‍♂️</span></div>
}

That was what we were doing for ages, until createRef comes to the game. React.createRef is more about ref2 way - current ref would just set to, well, ref.current.

Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render.

So - If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref instead. Ie the old way to _ref.

Hooks API Reference

const Example = () => {
   const [ref, setRef] = useState(null);
   const onRefSet = useCallback(ref => {
      setRef(ref);
      ref.current.focus(); // a side effect!
   });

   // well, you can re
   return <div ref={onRefSet}>😎</div>
}

But later you might try to combine ref-refs and callbacks-refs, and... well that's the road to 🔥hell🔥.

In addition - there is useImperativeHandle which partially could control ref propagation, but every time I was used to use it - it was just a 💩disaster💩.

function FancyInput(props, ref) {
  const inputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus(); // it just does not usually works :P
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

LET'S FIX IT!

Introducing use-callback-ref - the same createRef and useRef, but with callback built in.

import {useCallbackRef} from 'use-callback-ref';

const Example = () => {
   const ref = useCallbackRef(null, ref => ref && ref.focus());

   // that's all
   return <div ref={ref}>😎</div>
}

It's literally the old good ref with an on-change callback, nothing more.

Why not to use callback-based ref? Well, it's much easier to handle one interface, which would be accessible thought all components that ref would be passed, well, thought - while with setRef only callback would be visible for transitional components. However, that could be a good from isolation point of view.

This simple approach could also help with useImperativeHandle case:

function FancyInput(props, ref) {

  const inputRef = useCallbackRef(null, (newValue) => {
    // notice - this code is __isolated__, and you can move it off this component
    ref.current = { focus: () => newValue.focus() }
    // as long as you don't need to use callback-ref anymore - we could simply this case.
  });

  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

So - Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a useCallbackRef instead.

  • 300b, and IE11 support
  • based on getters and setters, no Proxies involved

Try it now(codesandbox demo), and call me back later - https://github.com/theKashey/use-callback-ref

And there is the second part of this article


Top comments (1)

Collapse
 
joelstransky profile image
Joel Stransky

This completely solved my issue with connecting Greensock to React-Pixi components.