用处
操作组件、子组件、元素的属性
在典型的
React
自上而下的数据流中,props
是父组件和子组件交互的唯一方式,要修改一个子组件,你需要使用新的props
来重新渲染它。但是在某些情况下,需要在典型的数据流之外强制修改子组件、获取组件的属性。被修改的子组件可能是一个React组件的实例,也可能是一个原生HTML
元素。
常用场景举例:
- 如何在组件中获取原生
HTML
元素属性、方法? - 如何在父组件中获取子组件的属性、方法?
- 如何在父组件中直接获取子组件中的某原生
HTML
元素?
Ref的四种创建方式
React.createRef()
在React
的16.3及以后的版本中,可以在实例的构造函数中使用React.createRef()
方法来创建ref
, 并将其赋值给实例对象的自定义属性,以便于在整个组件中都可以使用, 然后再将其附加给原生HTML
元素或Class
类组件的ref
属性上。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
回调函数方式
React
也支持另一种设置 ref
的方式,称为“回调 ref”。它能助你更精细地控制何时 refs
被设置和解除。
不同于通过createRef()
创建的ref
属性,“回调 refs”传递一个函数,并接受组件的实例对象或原生HTML DOM
对象作为参数,并将该参数赋值给组件实例对象的自定义属性,以便于在整个组件中都可以使用。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.setMyRef = null;
}
render() {
return < div ref={ (el) => this.setMyRef = el } />;
}
}
React
在组件挂载时会调用refs
回调函数,同时传入参数对象,在组件卸载时再次调用回调函数并传入null
作为参数,在componentDidMount
和 componentDidUpdate
触发前更新;
通过回调函数的方式, 能够直接在父组件中操作子组件中某元素的属性, 下面回详细提到:
字符串方式 (已过时、不推荐)
直接设置ref
属性为一个字符串, 通过this.refs
获取
const element = <div ref="myRef">string ref</div>
const node = this.refs.myRef
string
类型的 refs
用法已过时,且存在一些问题
- 需要
React
去保持跟踪当前渲染的组件,有点拖累性能 - 当使用
render callback
的渲染属性(render prop)模式渲染组件内容时, 为子组件设置ref
在父组件中获取不到, 容易造成误解。
class MyComponent extends Component {
renderRow = () => {
// 字符串设置的ref会被绑定到子组件DataTable上而不是父组件MyComponent
return <input ref="input" />;
}
render() {
return <DataTable renderRow={this.renderRow} />
}
}
//打印发现虽然在父组件中设置的ref, 但却不是绑定在父组件中,而是绑定在子组件中的refs中,
//如果通过回调函数设置,就可以避免该问题,
React.useRef()
在React
的16.8及以后的版本中,React
推出了Hooks
, 自此,我们可以通过Hooks
的方式为React
元素设置ref
属性, 由于Hooks
只能用于函数组件,所以useRef()
也只能为函数组件中的元素设置ref
.
function FocusInput() {
const inputEl = useRef();
//inputEl.current.focus();
return (
<>
<input ref={inputEl} type="text" />
</>
);
}
createRef()与useRef()区别
//useRef()
const inputEl = useRef();
<input ref={inputEl} type="text" />
//createRef()
const inputEl = createRef();
<input ref={inputEl} type="text" />
在使用上感觉没啥区别啊, 都是调用React
的内部方法,然后都是将返回值赋值给组件内部某元素的ref
属性,而且二者好像都能用在函数组件中。那React
为啥还要新出一个创建ref
的API
呢?
useRef
返回一个可变的 ref 对象,其.current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内持续存在。
Refs 是使用
React.createRef()
创建的,并通过ref
属性附加到 React 元素。在构造组件时,通常将Ref
分配给实例属性,以便可以在整个组件中引用它们。
这是官网对二者的解释, 是不是也没啥不同的地方,我们通过一个例子来体现二者的区别:在一个函数组件中同时通过这两种方式创建ref
function MyComponent() {
console.log("------------------------render--------------------------")
const [count, setCount] = useState(0);
const createRef = React.createRef();
const useRef = React.useRef();
console.log("createRef", createRef, "useRef",useRef)
if (!createRef.current) {
createRef.current = count;
}
if (!useRef.current) {
useRef.current = count;
}
console.log("createRef", createRef, "useRef",useRef)
return (
<div>
<h3>count: {count}</h3>
<button onClick={() => setCount(count + 1)}>add button</button>
</div>
);
}
在上面的函数组件中,每次的状态变更都会导致函数内的代码重新执行;同时我们也知道附加在元素身上的ref
属性,最终会通过其.current
属性体现出来,那么在每次count
状态变更时,打印的值会有什么不同吗?
在组件每次重新渲染时,createRef
生成的都是一个全新的对象,那么也就不会保存上一次的current
属性值; 而useRef
自始自终生成的都是同一个对象,或者说自始自终操作的的都是指向同一个对象的内存地址值;
ref转发
回到开篇时提出的三个问题,前两个都场景可以直接通过设置ref
获取对应元素或组件的属性;那么第三个问题,如何在父组件中直接获取子组件中的原生HTML
元素呢?
不同于组件普通的props
属性, 在父组件中设置的ref
不会透传下去,而是像key
一样,被React
接收后进行了特殊的处理, 不会出现在子组件的props
对象中:对子组件设置的ref
只会附加在当前子组件上, 不会向下传递, 此时通过ref
对象的.current
属性获取的是该子组件的实例对象。目前有两种方式:
-
回调ref
function Child(props) { return ( <input ref={props.childRef} /> ); } class MyComponent extends Component { handleClick() { this.childRef. console.dir(this.childRef) } render () { return ( <div> <Child childRef={el => this.childRef = el }></Child> <p onClick={() => this.handleClick()}>MyComponent</p> </div> ); } }
以上面代码为例: 在父组件
MyComponent
中给子组件Child
设置一个普通的可以往下传递的childRef
属性, 那么在子组件内部就可以通过props.childRef
获取该属性所对应的值;所以子组件内部的input
元素应该是这个样子<input ref={el => this.childRef = el} />
,而且这里的this
代表父组件,当子组件Child
被挂载时调用这个回调函数,并将input
元素的DOM对象
通过参数的形式传入该回调函数,这样的话在父组件中就能够直接操作子组件内部某HTML元素
的属性了。非常巧妙地运用函数闭包使二者之间产生联系。 通过字符串的方式就没办法做到了,所以这也是不推荐字符串方式设置ref
的原因之一。 -
ref转发
const Child = React.forwardRef((props, ref) => {
return <input ref={ref} defaultValue={props.defaultValue} />
} )
class MyComponent extends React.Component{
constructor(props) {
super(props)
this.childRef = React.createRef()
}
focus() {
this.childRef.current.focus()
}
render () {
return (
<div>
<Child defaultValue={'ref'} ref={this.childRef}></Child>
<button onClick={() => this.focus()}>focus</button>
</div>
);
}
}
通过React.forwardRef()
API对ref
属性进行转发,将ref
属性自动的通过组件传递到其子元素上的一种技巧;React.forwardRef()
接收一个函数作为参数,该函数的第二个参数接收组件的ref
属性,并能够将其向下传递;