How do we know when a React ref.current value has changed?

53,073

Solution 1

React docs recommend using callback refs to detect ref value changes.

Hooks

export function Comp() {
  const onRefChange = useCallback(node => {
    if (node === null) { 
      // DOM node referenced by ref has been unmounted
    } else {
      // DOM node referenced by ref has changed and exists
    }
  }, []); // adjust deps

  return <h1 ref={onRefChange}>Hey</h1>;
}

useCallback is used to prevent double calling of ref callback with null and the element.

You can trigger re-renders on change by storing the current DOM node with useState:

const [domNode, setDomNode] = useState(null);
const onRefChange = useCallback(node => {
  setDomNode(node); // trigger re-render on changes
  // ...
}, []);

Class component

export class FooClass extends React.Component {
  state = { ref: null, ... };

  onRefChange = node => {
    // same as Hooks example, re-render on changes
    this.setState({ ref: node });
  };

  render() {
    return <h1 ref={this.onRefChange}>Hey</h1>;
  }
}

Note: useRef doesn't notify of ref changes. Also no luck with React.createRef() / object refs.

Here is a test case, that drops and re-adds a node while triggering onRefChange callback :

const Foo = () => {
  const [ref, setRef] = useState(null);
  const [removed, remove] = useState(false);

  useEffect(() => {
    setTimeout(() => remove(true), 3000); // drop after 3 sec
    setTimeout(() => remove(false), 5000); // ... and mount it again
  }, []);

  const onRefChange = useCallback(node => {
    console.log("ref changed to:", node);
    setRef(node); // or change other state to re-render
  }, []);

  return !removed && <h3 ref={onRefChange}>Hello, world</h3>;
}

ReactDOM.render(<Foo />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.1/umd/react.production.min.js" integrity="sha256-vMEjoeSlzpWvres5mDlxmSKxx6jAmDNY4zCt712YCI0=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.1/umd/react-dom.production.min.js" integrity="sha256-QQt6MpTdAD0DiPLhqhzVyPs1flIdstR4/R7x4GqCvZ4=" crossorigin="anonymous"></script>

<script> var {useState, useEffect, useCallback} = React</script>

<div id="root"></div>

Solution 2

componentDidUpdate is invoked when the component state or props change, so it will not necessarily be invoked when a ref changes since it can be mutated as you see fit.

If you want to check if a ref has changed from previous render though, you can keep another ref that you check against the real one.

Example

class App extends React.Component {
  prevRef = null;
  ref = React.createRef();
  state = {
    isVisible: true
  };

  componentDidMount() {
    this.prevRef = this.ref.current;

    setTimeout(() => {
      this.setState({ isVisible: false });
    }, 1000);
  }

  componentDidUpdate() {
    if (this.prevRef !== this.ref.current) {
      console.log("ref changed!");
    }

    this.prevRef = this.ref.current;
  }

  render() {
    return this.state.isVisible ? <div ref={this.ref}>Foo</div> : null;
  }
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>
Share:
53,073
trusktr
Author by

trusktr

Joe Pea trusktr.io (personal site) lume.io (open source 3D toolkit I'm working on) twitter github codepen

Updated on July 09, 2022

Comments

  • trusktr
    trusktr almost 2 years

    Normally, with props, we can write

    componentDidUpdate(oldProps) {
      if (oldProps.foo !== this.props.foo) {
        console.log('foo prop changed')
      }
    }
    

    in order to detect prop changes.

    But if we use React.createRef(), how to we detect when a ref has changed to a new component or DOM element? The React docs don't really mention anything.

    F.e.,

    class Foo extends React.Component {
      someRef = React.createRef()
    
      componentDidUpdate(oldProps) {
        const refChanged = /* What do we put here? */
    
        if (refChanged) {
          console.log('new ref value:', this.someRef.current)
        }
      }
    
      render() {
        // ...
      }
    }
    

    Are we supposed to implement some sort of old-value thing ourselves?

    F.e.,

    class Foo extends React.Component {
      someRef = React.createRef()
      oldRef = {}
    
      componentDidMount() {
        this.oldRef.current = this.someRef.current
      }
    
      componentDidUpdate(oldProps) {
        const refChanged = this.oldRef.current !== this.someRef.current
    
        if (refChanged) {
          console.log('new ref value:', this.someRef.current)
    
          this.oldRef.current = this.someRef.current
        }
      }
    
      render() {
        // ...
      }
    }
    

    Is that what we're supposed to do? I would've thought that React would've baked in some sort of easy feature for this.