正在设置 React useImperativeHandle 和 forwardRef,引用似乎没有更新

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

我需要访问子组件的位置。据我了解,要访问子属性,我需要使用 useImperativeHandle 将子 API 添加到其引用中。此外,我需要使用 forwardRef 将引用从父级传递给子级。所以我这样做了:

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>

如您所见,ref 没有 getBoundingClientRect 属性,但如果我这样做,它将按预期工作:

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>

所以我对useImperativeHanedleforwardRef的理解有什么问题?

docs所示(可能不够理解),子组件本身应该有不同的ref,通过useImperativeHandle可以定义一个映射forwardedRef到子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"))

要使用 useImperativeHandle,您需要像这样使用另一个 ref 实例:

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>
  );
});

如果您希望您的逻辑有效(使用相同的转发ref),这将起作用:

const Text = React.forwardRef(({ onClick }, ref) => {
  React.useEffect(() => {
    ref.current.getLocation = ref.current.getBoundingClientRect;
  }, [ref]);

  return (
    <button onClick={onClick} ref={ref}>
      Press Me
    </button>
  );
});

为什么您的示例不起作用?

因为 ref.current.getBoundingClientRectuseImperativeHandle 中分配它时不可用(尝试记录它)因为你实际上用 useImperativeHandle 覆盖了按钮的 ref (检查 Text3 在沙箱中,ref.current 值在挂载后分配了 getLocation

我只是想添加这个答案来展示在删除无用的过度控制时事情如何变得更容易...

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>

在上面的代码中,我们只是使用 forwardRef 并将子 API 附加到它的 ref 上,这最终看起来很自然,并且非常用户友好。

唯一会阻止你使用它的是 React.createRef 调用 Object.preventExtension()(感谢你让我的生活更艰难......),所以 hack 是使用 { current: null } 而不是 Object.createRef() (基本相同)。