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

3

5

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?

Core answered 9/3, 2020 at 11:43 Comment(0)
Y
5

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

Yearning answered 9/3, 2020 at 12:8 Comment(8)
Thanks a lot! Could you explain why? I don't see what is actually going on that prevents my solution to work.Core
On your second example, wouldn't I risk to lose the getLocation function if the ref.current value gets updated ?Core
I updated the answer and yes, it is a risk (which you can handle), it just an example for how your logic might work, never seen such code in practiceYearning
But buttonRef.current.getBoundingClientRect is not available either, is it? This is where I don't see the difference...Core
I was mistaken, let me fix the answerYearning
Check the answer, I'm was surprised from the result too, you have all examples in the sandbox attached.Yearning
Ok, I reported the issue to React. Check my answer below to see how things could be made easier. Thanks for your time. You answered the question perfectly.Core
I found the idea of changing 'useImperativeHandler' to 'useEffect' very useful. This technique combined with the use of the custom hook 'useForwardedRef' described in this answer #73016196 is great!Mccleary
H
1

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"))
Halinahalite answered 9/3, 2020 at 12:6 Comment(0)
C
0

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).

Core answered 9/3, 2020 at 15:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.