Search code examples
javascriptreactjsreact-hooksreact-forwardref

React useImperativeHandle and forwardRef being set, the reference doesn't seem to be updated


I need to access the location of a child component. For what I understand, to access the child properties, I need to use useImperativeHandle to add the child API to its ref. Moreover, I need to use forwardRef to transmit the reference from the parent to the child. So I did this:

const Text = React.forwardRef(({ onClick }, ref) => {
  const componentAPI = {};
  componentAPI.getLocation = () => {
    return ref.current.getBoundingClientRect ? ref.current.getBoundingClientRect() : 'nope'
  };
  React.useImperativeHandle(ref, () => componentAPI);
  return (<button onClick={onClick} ref={ref}>Press Me</button>);
});

Text.displayName = "Text";

const App = () => {
  const ref = React.createRef();
  const [value, setValue] = React.useState(null)

  return (<div>
    <Text onClick={() => setValue(ref.current.getLocation())} ref={ref} />
    <div>Value: {JSON.stringify(value)}</div>
    </div>);
};

ReactDOM.render(<App />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="app"></div>

As you can see, the ref doesn't have the getBoundingClientRect property, but if I do this it will work as expected:

const App = () => {
  const ref = React.createRef();
  const [value, setValue] = React.useState(null)

  return (<div>
      <button ref={ref} onClick={() => setValue(ref.current.getBoundingClientRect()) } ref={ref}>Press Me</button>
      <div>Value: {JSON.stringify(value)}</div>
    </div>);
};

ReactDOM.render(<App />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="app"></div>

So what is wrong with my understanding of useImperativeHanedle and forwardRef?


Solution

  • To use useImperativeHandle you need to work with another ref instance like so:

    const Text = React.forwardRef(({ onClick }, ref) => {
      const buttonRef = React.useRef();
    
      React.useImperativeHandle(
        ref,
        () => ({
          getLocation: () => buttonRef.current.getBoundingClientRect()
        }),
        [buttonRef]
      );
    
      return (
        <button onClick={onClick} ref={buttonRef}>
          Press Me
        </button>
      );
    });
    

    If you want your logic to be valid (using the same forwarded ref), this will work:

    const Text = React.forwardRef(({ onClick }, ref) => {
      React.useEffect(() => {
        ref.current.getLocation = ref.current.getBoundingClientRect;
      }, [ref]);
    
      return (
        <button onClick={onClick} ref={ref}>
          Press Me
        </button>
      );
    });
    

    Why your example doesn't work?

    Because ref.current.getBoundingClientRect not available in a moment of assigning it in useImperativeHandle (try logging it) because you actually overridden the button's ref with useImperativeHandle (Check Text3 in sandbox, the ref.current value has getLocation assigned after the mount).

    Edit admiring-darkness-o8bm4