How to use ref callbacks in React

I'm sure you're familiar with the useRef hook.

And I'm willing to wager you're accustomed to using it like this

const inputRef = useRef<HtmlInputElement>(null);

return <input ref={inputRef} />;

And if you wanted to focus said input, you'd probably reach for an useEffect.

const inputRef = useRef<HtmlInputElement>(null);

useEffect(function focusInput() {
  inputRef.current?.focus();
}, []);

return <input ref={inputRef} />;

What if I told you there is a better way? 😏

Welcome ref callbacks

React refs are not just objects. They can be functions too!

type RefCallback<T> = {
  bivarianceHack(instance: T | null): void;
}["bivarianceHack"];
type Ref<T> = RefCallback<T> | RefObject<T> | null;

So we can do something like this

const inputRef = useRef<HtmlInputElement>(null);

<input ref={(el) => (inputRef.current = el)} />;

Basically, every render, this ref function will run and set the value with the variable el as a HtmlInputElement node.

But this is not really good is it? We don't want to re-run this over and over as an user interacts with our application.

Well, we can thank useCallback for coming to the rescue!

So, we have all the ingredients for a really nice cake 🍰

Let's mix them together!

Focusing an Input

const inputRef = useRef<HtmlInputElement>(null);

const inputRefCallback = useCallback(
  (el: HtmlInputElement | null) => {
    if (el) {
      el.focus();
    }
  },
  []
)

<input ref={inputRefCallback} />

Seriously, Remember memoization

Remember to use memoization so you're not focusing your input on every render, not only would that be wrong, it would be really annoying.

Measuring a container

type Dimensions = { width: number; height: number };

const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 });

const divRefCallback = useCallback(
  (el: HtmlDivElement | null) => {
    if (el) {
      const { width, height } = el.getBoundingClientRect();
      setDimensions({ width, height });
    }
  },
  []
)

<div ref={divRefCallback}>{children}</div>

Starting a game on a canvas

const canvasRefCallback = useCallback(
  (el: HtmlCanvasElement | null) => {
    if (el) {
      new GameEngine(el).start();
    }
  },
  []
)

<canvas ref={canvasRefCallback} />

And that's it! 🚀

Hope you learned something new and this becomes useful on your dev journey.