forwardRef转发Ref
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}> 元素
场景二:合并转发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中。
优点:
1.可以直接修改数据,不会造成函数组件冗余的更新作用。
2.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> }