浅谈 Refs 转发、forwardRef

3,793 阅读2分钟

前言

Ref转发,通俗说就是通过forwardRef这个API,将 ref 自动地从父组件传递给子组件的一种技巧。这种技术并不常见,使用场景主要有:

  • 转发ref到组件内部的DOM节点上
  • 在高阶组件中转发ref

上栗子

我们先写一个FancyButton组件:

const FancyButton = (props) => {
    return (
         <input style={{width: 210}} type="text"/>
         <button>{props.children}</button>
    );
}

在父组件App中渲染FancyButton

import * as React from 'react';
import * as ReactDOM from 'react-dom';

const App = () => {
    return (
        <div>
            <FancyButton>点我</FancyButton>
            <button>获取焦点</button>
        </div>
    );
}

ReactDOM.render(<App />, root);

现在我们希望,点击「获取焦点」,使FancyButton组件中的input聚焦,该如何解决?你可能会想到通过propsref属性从父组件传递给FancyButton组件,再绑定input,从而实现在父组件中操作子组件的 DOM节点。但是很遗憾,常规函数和 class 组件不接收ref参数,且props中也不存在ref

forwardRef出场了。

React.forwardRef

React.forwardRef(render)的返回值是react组件,接收的参数是一个render函数,函数签名为render(props, ref),第二个参数将其接收的 ref 属性转发到render返回的组件中。

const FancyButton = React.forwardRef((props, ref) => {
    return (
       <div>
           <input ref={ref} style={{width: 210}} type="text"/>
           <button>{props.children}</button></button>
       </div>
    );
});

这样,使用了FancyButton的组件就可以获取到底层DOM节点inputref,并在必要时进行访问。 完整代码如下:

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import {useRef} from 'react';

// 实现 ref 转发
const FancyButton = React.forwardRef((props, ref) => {
    return (
       <div>
           <input ref={ref} style={{width: 210}} type="text"/>
           <button>{props.children}</button></button>
       </div>
    )
})

const App = () => {
    const ref = useRef();
    
    // 在父组件中使用子组件的 ref
    const handleClick = () => {
        ref.current?.focus();
    }
    
    return (
        <div>
            <FancyButton ref={ref}>点我</FancyButton>
            <button onClick={handleClick}>获取焦点</button>
        </div>
    );
}

ReactDOM.render(<App />, root);

还没完!React.forwardRef通常配合useImperativeHandle使用。

useImperativeHandle

useImperativeHandle可以让我们在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用ref这样的命令式代码。useImperativeHandle应当与 forwardRef 一起使用。

useImperativeHandle(ref, createHandle, [deps])
  • ref:定义 current 对象的 ref
  • createHandle:一个函数,返回值是一个对象,即这个 ref 的 current
  • [deps]:依赖列表,当监听的依赖发生变化时,useImperativeHandle 才会重新将子组件的实例属性输出到父组件
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import {useRef, useImperativeHandle} from 'react';

const FancyInput = React.forwardRef((props, ref) => {
    const inputRef = useRef();
    
    useImperativeHandle(ref, () => ({
        focus: () => {
            inputRef.current?.focus();
        }
    }));
    
    return <input ref={inputRef} type="text"/>
});

const App = () => {
    const fancyInputRef = useRef();
    
    return (
        <div>
            <FancyInput ref={fancyInputRef}/>
            <button
                onClick={() => fancyInputRef.current?.focus()}
            >父组件调用子组件的 focus,实现 input 聚焦</button>
        </div>
    )
}

ReactDOM.render(<App />, root);

上面这个栗子中与直接转发ref不同,直接转发ref是将React.forwardRefrender 函数里的ref参数直接应用在了返回元素的ref属性上,父、子组件其实引用的是同一个refcurrent对象,官方不建议使用这样的ref透传。

而使用useImperativeHandle后,可以让父、子组件分别有自己的ref,通过React.forwardRef将父组件的ref透传过来,用useImperativeHandle方法来自定义开放给父组件的current

最后

如果文中有错误或者不足之处,欢迎大家在评论区指正。

你的点赞是对我莫大的鼓励!感谢阅读~