forwardRef的初衷就是解决ref不能跨层级捕获和传递的问题,forwardRef接受了父级元素标记的ref信息,并把它转发下去,使得子组件可以通过props来接受到上一层级或者更上层级的ref。
比如想要通过标记子组件ref,来获取子组件下的孙组件的某一DOM元素,或者是组件实例。使用 React.forwardRef
场景:想要在GrandFather组件通过标记ref,获取到孙组件Son的组件实例.可以理解为,React.forwardRef将子组件的ref跑出去到顶层。在爷爷组件中可以进行使用
// 孙组件
function Son(Props) {
const {grandRef} = props
return <div>
<div>i am alien</div>
<span ref={grandRef}>123</span>
</div>
}
// 父组件
class Father extends React.Component{
constructor(props){
super(props)
}
render () {
return <div>
<Son grandRef={this.props.grandRef}></Son>
</div>
}
}
const newFather = React.forwardRef((props,ref) => <Father grandRef={ref} {...props}></Father>)
// 爷组件
class GrandFather extends React.Component {
constructor(props){
super(props)
}
node = null
componentDidMount(){
console.log(this.node)
}
render () {
return <div>
<NewFather ref={(node) => this.node = node}></NewFather>
</div>
}
}
如上所述:forwardRef把ref变成了可以通过props传递和转发。
官网解释:
React.forwardRef 会创建一个React组件,这个组件能够将其接受的ref属性转发到其组件树下的另一个组件中。
React.forwardRef接受渲染函数作为参数,React将使用props和ref作为参数来调用次函数,此函数返回React节点。
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
上述中,React会将<FancyButton ref={ref}>元素的 ref作为第二个参数传递给React.forwardRef函数中的渲染函数,该渲染函数会将ref 传递给<button ref={ref}> 元素
通过forwardRef转发的ref不要理解为只能用来直接获取组件实例,DOM元素,也可以用来传递合并之后的自定义ref。
场景:通过Home绑定ref,来获取子组件Index的实例Index,dom元素button,以及孙组件Form的实例。
// 表单组件
class Form extends React.Component{
render() {
return <div>{...}</div>
}
}
// index组件
class Index extends React.Component{
componentDidMount() {
const {forwardRef} = this.props
forwardRef,current = {
form: this.form,
index: this,
button: this.button
}
}
form = null
button = null
render() {
return <div>
<button ref={(button) => this.button = button}>点击</button>
<Form ref={(form) => this.form = form}></Form>
</div>
}
}
const ForwardRefIndex = React.forwardRef((props,ref) => <Index {...props} forwardRef={ref} ></Index>)
// home组件
export default function Home() {
const ref = useRef(null)
useEffect(() => {
console.log(ref.current)
},[])
return <ForwardRefIndex ref={ref}></ForwardRefIndex>
}
如上所示:
- 通过useRef创建了一个ref对象,通过forwardRef将当前ref对象传递给子组件。
- 向Home 组件传递的ref对象上,绑定form孙组件实例,index子组件实例,和button DOM元素。
如果通过高阶组件包裹一个原始类组件,就会产生一个问题,如果高阶组件HOC没有处理ref,那么由于高阶组件本身会返回一个新的组件,所以当使用HOC包装后组件的时候,标记的ref会指向HOC返回的组件,而并不是HOC包裹的原始类组件,为了解决这个问题,forwardRef可以对HOC做一层处理。
function HOC(Component){
class Wrap extends React.Component{
render(){
const { forwardedRef ,...otherprops } = this.props
return <Component ref={forwardedRef} {...otherprops} />
}
}
return React.forwardRef((props,ref)=> <Wrap forwardedRef={ref} {...props} /> )
}
class Index extends React.Component{
render(){
return <div>hello,world</div>
}
}
const HocIndex = HOC(Index)
export default ()=>{
const node = useRef(null)
useEffect(()=>{
console.log(node.current) /* Index 组件实例 */
},[])
return <div><HocIndex ref={node} /></div>
}
经过forwardRef 处理后的HOC,就可以正常访问到Index组件实例。
- 声明一个组件Wrap,接收 HOC 传入的组件Index。
- 使用forwardRef 进行ref转发。
- 在HocIndex中获取到Index实例。
ref实现组件通讯
在一些场景下不想通过往常的单数据流的方式来改变子组件的更新,拿着时候就可以通过ref模式标记子组件实例。,从而操作子组件。如antd的form表单,对外暴漏出来的 resetFields,setFieldsValue等接口。
对于类组件可以通过ref直接获取组件实例。实现组件通信
/* 子组件 */
class Son extends React.PureComponent{
state={
fatherMes:'',
sonMes:''
}
fatherSay=(fatherMes)=> this.setState({ fatherMes }) /* 提供给父组件的API */
render(){
const { fatherMes, sonMes } = this.state
return <div className="sonbox" >
<div className="title" >子组件</div>
<p>父组件对我说:{ fatherMes }</p>
<div className="label" >对父组件说</div> <input onChange={(e)=>this.setState({ sonMes:e.target.value })} className="input" />
<button className="searchbtn" onClick={ ()=> this.props.toFather(sonMes) } >to father</button>
</div>
}
}
/* 父组件 */
export default function Father(){
const [ sonMes , setSonMes ] = React.useState('')
const sonInstance = React.useRef(null) /* 用来获取子组件实例 */
const [ fatherMes , setFatherMes ] = React.useState('')
const toSon =()=> sonInstance.current.fatherSay(fatherMes) /* 调用子组件实例方法,改变子组件state */
return <div className="box" >
<div className="title" >父组件</div>
<p>子组件对我说:{ sonMes }</p>
<div className="label" >对子组件说</div> <input onChange={ (e) => setFatherMes(e.target.value) } className="input" />
<button className="searchbtn" onClick={toSon} >to son</button>
<Son ref={sonInstance} toFather={setSonMes} />
</div>
}
父组件使用 props传递值,将子组件给父组件要说的话的方法传递给子组件,供子组件给父组件进行传递。
子组件中写好父组件要给子传递值的方法,。并且将自己的实例通过ref暴漏给父组件,父组件通过暴漏的实例上的对应方法进行传递。
useImperativeHandle 在函数组件中获取子组件ref实例。
它有三个参数:
- 第一个参数 ref : 接受 forWardRef 传递过来的 ref 。
- 第二个参数 createHandle :处理函数,返回值作为暴露给父组件的 ref 对象。
- 第三个参数 deps :依赖项 deps,依赖项更改形成新的 ref 对象。
流程分析:
- 上述代码中,父组件用ref标记了子组件ForwarSon,但是子组件Son是函数组件,没有实例返回。所以用forwardRef 转发ref生成了一个新的组件,ForwarSon。
- 子组件Son用useImperativeHandle接收父组件ref生成Son实例,并将input的方法 onFocus 和改变input输入值的方法 onChange 传递给了ref,对外暴漏了出去。
- 父组件可以通过ref调用 onFocus 以及 onChangevalue
使用useRef 做函数组件缓存
函数组件每一次render,函数都会上下文重新执行,有一种情况就是,在执行一些事件方法改变数据或者保存新数据的时候,没必要更新视图,这时候就无需放在state中。
useRef 可以创建出一个ref原始对象,只要组件没有销毁,ref对象就一直存在,那么完全可以把一些不依赖于视图更新的数据存储到ref中。
优点:
- 可以直接修改数据,不会造成函数组件冗余的更新作用。
- useRef保存数据,它始终在当前组件中指向一个内存空间,这样的好处是随时可以访问到变化后的值。
const toLearn = [ { type: 1 , mes:'let us learn React' } , { type:2,mes:'let us learn Vue3.0' } ]
export default function Index({ id }){
const typeInfo = React.useRef(toLearn[0])
const changeType = (info)=>{
typeInfo.current = info /* typeInfo 的改变,不需要视图变化 */
}
useEffect(()=>{
if(typeInfo.current.type===1){
/* ... */
}
},[ id ]) /* 无须将 typeInfo 添加依赖项 */
return <div>
{
toLearn.map(item=> <button key={item.type} onClick={ changeType.bind(null,item) } >{ item.mes }</button> )
}
</div>
}