question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

React callback ref cleanup function

See original GitHub issue

At the time React added callback refs the main use case for them was to replace string refs. A lot of the callback refs looked like this:

<div ref={node => this.node = node} />

With the introduction of createRef and useRef this use case is pretty much replaced by these alternatives so the use case of callback refs will shift to advanced use cases like measuring DOM nodes.

It would be nice if you could return a cleanup function from the callback ref which is called instead of the callback with null. This way it will behave more like the useEffect API.

<div ref={node => {
  // Normal ref callback

  return () => {
    // Cleanup function which is called when the ref is removed
  }
}} />

This will be super helpful when you need to set up a Resize-, Intersection- or MutationObserver.

function useDimensions() {
  const [entry, setEntry] = useState()
  
  const targetRef = useCallback((node) => {
    const observer = new ResizeObserver(([entry]) => {
      setEntry(entry)
    })

    observer.observe(node)

    return () => {
      observer.disconnect()
    }
  }, [])

  return [entry, targetRef]
}

function Comp() {
  const [dimensions, targetRef] = useDimensions()

  return (
    <pre ref={targetRef}>
      {JSON.stringify(dimensions, null, 2)}
    </pre>
  )
}

Currently, if you want to implement something like this you need to save the observer into a ref and then if the callback ref is called with null you have to clean up the observer from the ref.

To be 99% backward compatible we could call both the callback ref with null and the cleanup function. The only case where it isn’t backward compatible is if currently someone is returning a function and doesn’t expect the function to be called.

function ref(node) {
  if (node === null) {
    return
  }

  // Do something

  return () => {
    // Cleanup something
  }
}

Issue Analytics

  • State:open
  • Created 5 years ago
  • Reactions:55
  • Comments:17 (2 by maintainers)

github_iconTop GitHub Comments

18reactions
butchlercommented, Jul 18, 2019

Yet another attempt at implementing this as a custom hook:

import { useRef, useCallback } from 'react';

export default function useCallbackRef(rawCallback) {
  const cleanupRef = useRef(null);
  const callback = useCallback((node) => {
    if (cleanupRef.current) {
      cleanupRef.current();
      cleanupRef.current = null;
    }

    if (node) {
      cleanupRef.current = rawCallback(node);
    }
  }, [rawCallback]);

  return callback;
}

Usage:

const callback = useCallbackRef(useCallback((node) => {
  node.addEventListener(...);
  return () => {
    node.removeEventListener(...);
  };
}, []));

It’s a bit more cumbersome to use, since you have to call both useCallback and useCallbackRef, but at least it allows the deps to be checked by the exhaustive-deps linting rule.

@k15a What do you think of this approach?

6reactions
KurtGokhancommented, Sep 8, 2021

I am going to mention one use-case that cannot be solved by any of the workarounds listed here. It happens when you want to use the same ref callback for multiple elements.

As a simple example, let’s say I want to write a ref that adds all elements to an array, and remove them from the array when they are removed from DOM. I should be able to simply write:

const list = [];

function Test() {
  const register = useCallback((el) => {
    if (!el) return;

    list.push(el);
    
    return () => {
      const index = list.indexOf(el);
      if (index >= 0) list.splice(index, 1);
    };
  }, []);

  return <>
    <span ref={register} />
    <span ref={register} />
    <span ref={register} />
  </>;
}

I don’t know if other people would think of this use-case as a valid usage of ref, but I think it is a beautiful code pattern that both saves performance and provides an easy API.

As far as I know, there is no easy workaround for this. One library I know which uses this kind of pattern is react-hook-form. It solves the issue by making register a function and calling it with a unique name for each element like <input ref={register("password")} />. As for me, I was going to build a reusable tooltip hook but it’s not going to be that easy without this functionality.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Cleanup ref issues in React - Stack Overflow
Solution is to use componentDidUpdate (class-based component) or another useEffect hook with appropriate dependency, to update whatever local ...
Read more >
Avoiding useEffect with callback refs - TkDodo's blog
ref is a reserved property on build-in primitives, where React will store the DOM node after it was rendered. It will be set...
Read more >
Understanding React's useEffect cleanup function
React's useEffect cleanup function saves applications from unwanted behaviors like memory leaks by cleaning up effects.
Read more >
Hooks API Reference - React
useReducer; useCallback; useMemo; useRef; useImperativeHandle ... To do this, the function passed to useEffect may return a clean-up function.
Read more >
Your Guide to React.useCallback() - Dmitri Pavlutin
2. The purpose of useCallback() · A functional component wrapped inside React.memo() accepts a function object prop · When the function object is ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found