在典型的 React 数据流中,props是父子组件交互的唯一方式。要修改一个子组件,需要使用新的 props 重新渲染它。但是,在某些情况下,需要在典型数据流之外主动查看或强制修改子组件,这时候就需要使用 Refs,将 DOM Refs 暴露给父组件。
何时使用 Refs
下面是几个适合使用 refs 的情况:
- 管理焦点,文本选择或媒体播放;
- 触发强制动画;
- 集成第三方 DOM 库;
- 测量子 DOM 节点的大小或位置;
避免使用 refs 来做任何可以通过声明式实现来完成的事情,因为它会打破组件的封装。
Refs 与组件
默认情况下,ref
属性必须指向一个 DOM 元素或 class 组件,不能在函数组件上使用 ref
属性,因为它们没有实例。如果需要在函数组件中使用 ref
,可以使用 forwardRef
,或者将该组件转化为 class 组件。
React.forwardRef
React.forwardRef(props, ref)
- 第二个参数
ref
只在使用React.forwardRef
定义组件时存在。常规函数和 class 组件不接收ref
参数,且 props 中也不存在ref
。 - Ref 转发不仅限于 DOM 组件,也可以转发 refs 到 class 组件实例中。
Ref 转发
如果使用 16.3 以上版本的 React,使用 ref 转发将 DOM Refs 暴露给父组件。Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref。如果对子组件的实现没有控制权的话,只能使用 findDOMNode()
,但在严格模式下已被废弃且不推荐使用。
实现 Ref 转发方式:
- ref 和 forwardRef
- useImperativeHandle 和 forwardRef
ref 和 forwardRef
- 父组件创建
ref
,并向下传递给子组件 - 子组件通过
forwardRef
来获取传递给它的ref
- 子组件拿到
ref
并向下转发该ref
到自己的某一个元素上 - 父组件通过
ref.current
获取绑定的 DOM 元素实例
缺点
- 会把绑定 ref 元素的 DOM 直接暴露给了父组件
- 父组件拿到 DOM 后可以进行任意的操作
例子
下面的例子使用的是 React 16.6 引入的 useRef Hook,如果是 React 16.3 版本使用 React.createRef()
API, 如果更早之前的版本,使用回调形式的refs。
// 子组件
import React, { forwardRef, useState } from 'react'
const BaseInfo = forwardRef((props, ref) => {
console.log('BaseInfo --- props', props)
console.log('BaseInfo --- ref', ref)
const [name, setName] = useState('')
const [age, setAge] = useState('')
return (
<div ref={ref}>
姓名:
<input onChange={val => setName(val)} />
年龄:
<input onChange={val => setAge(val)} />
</div>
)
})
// 父组件
import React, { useRef } from 'react'
import BaseInfo from './BaseInfo'
function RefTest() {
const baseInfoRef = useRef(null)
const getBaseInfo = () => {
console.log('getBaseInfo --- baseInfoRef', baseInfoRef.current)
}
return (
<>
<BaseInfo
dataSource={{ name: '混沌' }}
ref={baseInfoRef}
/>
<button onClick={getBaseInfo}>获取基本信息</button>
</>
)
}
输出
点击“获取基本信息”按钮后:
useImperativeHandle 和 forwardRef
useImperativeHandle 介绍
useImperativeHandle(ref, createHandle, [deps])
使用 ref
时通过useImperativeHandle
自定义暴露给父组件的实例值。
通过 useImperativeHandle, 将父组件传入的 ref 和 useImperativeHandle 第二个参数返回的对象绑定到了一起。
优点
- 只暴露给父组件需要用到的 DOM 方法;
- 在父组件中, 调用 xxxRef.current 时,返回的是对象;
例子
// 子组件
const OtherInfo = (props, ref) => {
console.log('OtherInfo --- props', props)
console.log('OtherInfo --- ref', ref)
const inputRef = useRef()
const [school, setSchool] = useState('')
useImperativeHandle(ref, () => {
console.log('useImperativeHandle --- ref', ref)
return ({
focus: () => {
inputRef.current.focus()
},
getSchool: () => {
return inputRef.current.value
}
})
}, [inputRef])
return (
<div>
学校:
<input ref={inputRef} onChange={val => setSchool(val)}/>
</div>
)
}
export default forwardRef(OtherInfo)
// 父组件
import React, { useRef } from 'react'
import OtherInfo from './OtherInfo'
function RefTest() {
const otherInfoRef = useRef(null)
const getOtherInfo = () => {
console.log('getOtherInfo --- otherInfoRef', otherInfoRef.current)
console.log('getOtherInfo --- otherInfoRef --- getSchool', otherInfoRef.current.getSchool())
}
return (
<>
<OtherInfo
dataSource={{ school: '大学' }}
ref={otherInfoRef}
/>
<button onClick={getOtherInfo}>获取其他信息</button>
</>
)
}
输出
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。