Sharcoux
Sharcoux

Reputation: 6085

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?

Upvotes: 6

Views: 18918

Answers (3)

Sharcoux
Sharcoux

Reputation: 6085

I just wanted to add this answer to show how things can become easier when removing useless overcontrol...

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

Text.displayName = "Text";

function App() {
  const ref = { current: null };
  const [value, setValue] = React.useState(null)
  return (<div>
    <Text onClick={() => setValue(ref.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>

In the code above, we just use forwardRef and attach the child API to it's ref, which seems very natural in the end, and very userfriendly.

The only thing that would prevent you using this is that React.createRef makes a call to Object.preventExtension() (thanks for making my life harder...), so the hack is to use { current: null } instead of Object.createRef() (which is basically the same).

Upvotes: 0

Dennis Vash
Dennis Vash

Reputation: 53884

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

Upvotes: 5

StackedQ
StackedQ

Reputation: 4139

As shown in docs(maybe not understandable enough), the child component itself should have a different ref, and by useImperativeHandle you can define a function mapping forwardedRef to child ref:

import React from 'react'
import ReactDOM from 'react-dom'
const Text = React.forwardRef(({ onClick }, ref) => {
  const buttonRef = React.useRef() // create a new ref for button
  const componentAPI = {};
  componentAPI.getLocation = () => {
    return buttonRef.current.getBoundingClientRect ? buttonRef.current.getBoundingClientRect() : 'nope' // use buttonRef here
  };
  React.useImperativeHandle(ref, () => componentAPI); // this maps ref to buttonRef now
  return (<button onClick={onClick} ref={buttonRef}>Press Me</button>); // set buttonRef
});

Text.displayName = "Text";

const App = () => {
  const ref = React.useRef();
  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"))

Upvotes: 1

Related Questions