前言
本文为意译, 翻译过程中掺杂本人的理解, 如有误导, 请放弃继续阅读.
Ref forwarding 是一种将 ref 钩子自动传递给组件的子孙组件的技术. 对于应用的大部分组件, 这种技术并不是那么的必要. 然而, 它对于个别的组件还是特别地有用的, 尤其是那些可复用组件的类库. 下面的文档讲述的是这种技术的最常见的应用场景.
正文
传递 refs 到 DOM components
假设我们有一个叫 FancyButton 的组件, 它负责在界面上渲染出一个原生的 DOM 元素 - button:
- function FancyButton(props) {
- return (
- <button className="FancyButton">
- {props.children}
- </button>
- );
- }
一般意义来说, React 组件就是要隐藏它们的实现细节, 包括自己的 UI 输出. 而其他引用了 < FancyButton > 的组件也不太可能想要获取 ref, 然后去访问 < FancyButton > 内部的原生 DOM 元素 button. 在组件间相互引用的过程中, 尽量地不要去依赖对方的 DOM 结构, 这属于一种理想的使用场景.
对于一些应用层级下的组件, 比如 < FeedStory > 和 < Comment > 组件 (原文档中, 没有给出这两个组件的实现代码, 我们只能顾名思义了), 这种封装性是我们乐见其成的. 但是, 这种封装性对于达成某些 "叶子"(级别的) 组件 (比如,<FancyButton > 和 < MyTextInput>) 的高可复用性是十分的不方便的. 因为在项目的大部分场景下, 我们往往是打算把这些 "叶子" 组件都当作真正的 DOM 节点 button 和 input 来使用的. 这些场景可能是管理元素的聚焦, 文本选择或者动画相关的操作. 对于这些场景, 访问组件的真正 DOM 元素是在所难免的了.
Ref forwarding 是组件一个可选的特征. 一个组件一旦有了这个特征, 它就能接受上层组件传递下来的 ref, 然后顺势将它传递给自己的子组件.
在下面的例子当中,<FancyButton > 通过 React.forwardRef 的赋能, 它可以接收上层组件传递下来的 ref, 并将它传递给自己的子组件 - 一个原生的 DOM 元素 button:
- const FancyButton = React.forwardRef((props, ref) => (
- <button ref={ref} className="FancyButton">
- {props.children}
- </button>
- ));
- // 假如你没有通过 React.createRef 的赋能, 在 function component 上你是不可以直接挂载 ref 属性的.
- // 而现在你可以这么做了, 并能访问到原生的 DOM 元素:
- const ref = React.createRef();
- <FancyButton ref={ref}>Click me!</FancyButton>;
通过这种方式, 使用了 < FancyButton > 的组件就能通过挂载 ref 到 < FancyButton > 组件的身上来访问到对应的底层的原生 DOM 元素了 - 就像直接访问这个 DOM 元素一样.
下面我们逐步逐步地来解释一下上面所说的是如何发生的:
我们通过调用 React.createRef 来生成了一个 React ref https://reactjs.org/docs/refs-and-the-dom.html , 并且把它赋值给了 ref 变量.
我们通过手动赋值给 < FancyButton > 的 ref 属性进一步将这个 React ref 传递下去.
接着, React 又将 ref 传递给 React.forwardRef()调用时传递进来的函数
(props, ref) => ...
. 届时, ref 将作为这个函数的第二个参数.
在
(props, ref) => ...
组件的内部, 我们又将这个 ref 传递给了作为 UI 输出一部分的 < button ref={ref}>组件.
当 < button ref={ref}>组件被真正地挂载到页面的时候,, 我们就可以在使用 ref.current 来访问真正的 DOM 元素 button 了.
注意, 上面提到的第二个参数 ref 只有在你通过调用 React.forwardRef()来定义组件的情况下才会存在. 普通的 function component 和 class component 是不会收到这个 ref 参数的. 同时, ref 也不是 props 的一个属性.
Ref forwarding 技术不单单用于将 ref 传递到 DOM component. 它也适用于将 ref 传递到 class component, 以此你可以获取这个 class component 的实例引用.
组件类库维护者的注意事项
当你在你的组件类库中引入了 forwardRef, 那么你就应该把这个引入看作一个 breaking change, 并给你的类库发布个 major 版本. 这么说, 是因为一旦你引入了这个特性, 那你的类库将会表现得跟以往是不同( 例如: what refs get assigned to, and what types are exported), 这将会打破其他依赖于老版 ref 功能的类库和整个应用的正常功能.
我们得有条件地使用 React.forwardRef, 即使有这样的条件, 我们也推荐你能不用就不要用. 理由是: React.forwardRef 会改变你类库的行为, 并且会在用户升级 React 版本的时候打破用户应用的正常功能.
高阶组件里的 Forwarding refs
这种技术对于高阶组件来说也是特别有用的. 假设, 我们要实现一个打印 props 的高阶组件, 以往我们是这么写的:
- function logProps(WrappedComponent) {
- class LogProps extends React.Component {
- componentDidUpdate(prevProps) {
- console.log('old props:', prevProps);
- console.log('new props:', this.props);
- }
- render() {
- return <WrappedComponent {...this.props} />;
- }
- }
- return LogProps;
- }
高阶组件 logProps 将所有的 props 都照样传递给了 WrappedComponent, 所以高阶组件的 UI 输出和 WrappedComponent 的 UI 输出将会一样的. 举个例子, 我们将会使用这个高阶组件来把我们传递给 < FancyButton > 的 props 答应出来.
- class FancyButton extends React.Component {
- focus() {
- // ...
- }
- // ...
- }
- // Rather than exporting FancyButton, we export LogProps.
- // It will render a FancyButton though.
- export default logProps(FancyButton);
上面的例子有一个要注意的地方是: refs 实际上并没有被传递下去 (到 WrappedComponent 组件中). 这是因为 ref 并不是真正的 prop. 正如 key 一样, 它们都不是真正的 prop, 而是被用于 React 的内部实现. 像上面的例子那样给一个高阶组件直接传递 ref, 那么这个 ref 指向的将会是(高阶组件所返回) 的 containercomponent 实例而不是 wrapper component 实例:
- import FancyButton from './FancyButton';
- const ref = React.createRef();
- // The FancyButton component we imported is the LogProps HOC.
- // Even though the rendered output will be the same,
- // Our ref will point to LogProps instead of the inner FancyButton component!
- // This means we can't call e.g. ref.current.focus()
- <FancyButton
- label="Click Me"
- handleClick={handleClick}
- ref={ref}
- />;
幸运的是, 我们可以通过调用 React.forwardRef 这个 API 来显式地传递 ref 到 FancyButton 组件的内部. React.forwardRef 接收一个 render function, 这个 render function 将会得到两个实参: props 和 ref. 举例如下:
- function logProps(Component) {
- class LogProps extends React.Component {
- componentDidUpdate(prevProps) {
- console.log('old props:', prevProps);
- console.log('new props:', this.props);
- }
- render() {
- + const {forwardedRef, ...REST} = this.props;
- // Assign the custom prop "forwardedRef" as a ref
- + return <Component ref={forwardedRef} {...REST} />;
- - return <Component {...this.props} />;
- }
- }
- // Note the second param "ref" provided by React.forwardRef.
- // We can pass it along to LogProps as a regular prop, e.g. "forwardedRef"
- // And it can then be attached to the Component.
- + return React.forwardRef((props, ref) => {
- + return <LogProps {...props} forwardedRef={ref} />;
- + });
- }
在 DevTools 里面显示一个自定义的名字
React.forwardRef 接收一个 render function.React DevTools 将会使用这个 function 来决定将 ref forwarding component 名显示成什么样子.
举个例子, 下面的 WrappedComponent 就是 ref forwarding component. 它在 React DevTools 将会显示成 "ForwardRef":
- const WrappedComponent = React.forwardRef((props, ref) => {
- return <LogProps {...props} forwardedRef={ref} />;
- });
假如你给 render function 命名了, 那么 React DevTools 将会把这个名字包含在 ref forwarding component 名中(如下, 显示为 "ForwardRef(myFunction)"):
- const WrappedComponent = React.forwardRef(
- function myFunction(props, ref) {
- return <LogProps {...props} forwardedRef={ref} />;
- }
- );
你甚至可以把 wrappedComponent 的名字也囊括进来, 让它成为 render function 的 displayName 的一部分(如下, 显示为 "ForwardRef(logProps(${ http://wrappedComponent.name }))"):
- function logProps(Component) {
- class LogProps extends React.Component {
- // ...
- }
- function forwardRef(props, ref) {
- return <LogProps {...props} forwardedRef={ref} />;
- }
- // Give this component a more helpful display name in DevTools.
- // e.g. "ForwardRef(logProps(MyComponent))"
- const name = Component.displayName || Component.name;
- forwardRef.displayName = `logProps(${name})`;
- return React.forwardRef(forwardRef);
- }
这样一来, 你就可以看到一条清晰的 refs 传递路径: React.forwardRef -> logProps -> wrappedComponent. 如果这个 wrappeedComponent 是我们上面用 React.forwardRef 包裹的 FancyButton, 这条路径可以更长: React.forwardRef -> logProps -> React.forwardRef -> FancyButton -> button.
来源: https://juejin.im/post/5c0dd44b51882530e4617e92