简介
本文从以下几个方面介绍React组件的refs属性
- 什么是refs
- 类组件中使用refs的几种方式
- 函数式组件中使用refs的方式
- 总结refs
什么是refs
在React中,ref
是一种用于访问组件的底层DOM节点或React元素的方式。它允许你直接访问和操作组件内部的DOM元素,或者引用React组件的实例。ref
的主要用途包括:
- 访问DOM元素 :你可以使用
ref
来获取DOM元素的引用,以便在需要时直接操作它们,例如改变样式、聚焦、滚动等。 - 访问React组件的实例 :你可以使用
ref
来获取对React组件的实例的引用,以便在需要时直接调用组件的方法或访问其属性。 - 触发组件生命周期方法:通过获取组件的实例,你可以手动调用组件的生命周期方法,以模拟组件的生命周期事件。
类组件中使用ref
在类组件中有字符串形式的 ref
、React.createRef()
、回调函数形式的 ref
三种方式创建refs,我们推荐使用后两种方式。
字符串形式的 ref
这是较旧的一种方式,已经被官方淘汰了。不想了解的小伙伴可以直接跳到下一个。
它允许你为组件的 ref
属性分配一个字符串,然后你可以通过 this.refs
来访问它。
jsx
class MyComponent extends React.Component {
componentDidMount() {
console.log(this.refs.myRef1)
console.log(this.refs.myRef2)
}
render() {
return (
<div>
<input ref="myRef1" />
<input ref="myRef2" />
</div>
);
}
}
React.createRef()
React.createRef()
是 React 推荐的用于创建一个 ref
对象的方法。React.createRef()
返回的是一个 ref
对象,该对象可以被赋值给 React 元素的 ref
属性,从而允许你访问该元素或组件实例。使用方法如下:
1. 创建 Ref 对象:
使用 React.createRef()
创建一个 ref 对象,通常在类组件的构造函数中进行初始化。例如:
jsx
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
// ...
}
2. 将 Ref 与 React 元素关联:
在 JSX 中,将创建的 ref 对象赋值给 React 元素的 ref
属性,以建立关联。这通常在组件的 render
方法中完成。例如:
jsx
render() {
return <div ref={this.myRef}>Hello, World!</div>;
}
在上面的例子中,this.myRef
绑定了 <div>
元素。
3. 访问 Ref 中的当前值:
通过 this.myRef.current
属性,我们可以访问与 ref
关联的元素或组件实例。例如:
jsx
componentDidMount() {
console.log(this.myRef.current); // 访问关联的元素或组件实例
}
这是在组件挂载后访问 ref 中的当前值的常见方式。
用途:
React.createRef()
主要用于访问React元素的底层DOM节点或类组件的实例。你可以在需要时操作这些元素或实例,例如修改样式、获取或修改输入值、执行原生DOM操作,或调用组件的方法。
注意事项:
- Refs是可变的,因此你需要小心在访问它们之前检查它们是否存在。
- 不要在函数组件中使用
React.createRef()
,因为它通常与类组件的生命周期方法结合使用。在函数组件中,通常使用useRef
钩子来创建ref
。
创建创建多个ref
,多次调用React.createRef()
,会返回不同的引用,如下例子
jsx
constructor(props) {
super(props);
// 创建多个 ref 对象
this.inputRef = React.createRef();
this.divRef = React.createRef();
this.customComponentRef = React.createRef();
}
原因: 多次调用 React.createRef()
会生成不同的 ref 对象,每个对象都有其自己的 current
属性,用于引用与其关联的元素或实例。这样可以轻松管理多个不同的引用于不同的DOM元素或React组件实例。
总之,React.createRef()
是一个用于在React类组件中创建和管理引用的工具,允许你访问和操作React元素或组件实例。
回调函数形式的 ref
回调函数形式的 ref
是将 ref
属性设置为一个回调函数 。这个回调函数接收一个参数 node
,代表渲染的 input
元素。
jsx
class MyComponent extends React.Component {
componentDidMount() {
// 在组件挂载后,将按钮元素的文本内容修改为"点击我"。
this.myButton.textContent = "点击我";
}
render() {
return (
<div>
<button
ref={(node) => {
this.myButton = node;
}}
>
初始文本
</button>
</div>
);
}
}
函数式组件的ref
在 React 16.3 之前的版本中,函数式组件确实无法直接接收 ref,因为它们没有实例。然而,自 React 16.3 版本开始,React 引入了 forwardRef
、useRef
等函数,使得函数式组件也可以接收 ref
。
useRef
在 React 的函数式组件中,主要使用 useRef
这个 Hook 来创建一个可持久化的ref
。useRef
返回一个可变的 ref
对象,并且在组件的多次渲染之间保持不变。它主要用于获取或修改 DOM 节点、保存组件内部变量等场景。
useRef
的用法和特点如下:
-
创建和访问
ref
:通过调用useRef(initialValue)
来创建一个 ref 对象,并传入初始值。例如:const myRef = useRef(initialValue);
。- 创建的 ref 对象可以在组件的整个生命周期中持久存在,可以在组件的多个渲染中获取到相同的 ref 对象。
- 可以通过
myRef.current
来访问或修改 ref 的当前值。
-
获取和操作 DOM 节点:将
useRef
与 JSX 中的ref
属性结合使用,可以获取组件或 DOM 元素的引用。- 在函数组件中,通过将
ref
属性赋值为useRef
创建的 ref 对象,就可以在current
属性中获取到对应的 DOM 节点。 - 例如:
<div ref={myRef}>...</div>
,然后可以通过myRef.current
来访问该<div>
元素。
- 在函数组件中,通过将
-
保存组件内部变量:由于
ref
对象在组件的多次渲染之间保持不变,可以将一些需要在组件生命周期中保持持久化的变量保存在ref
中。- 对于函数组件而言,由于每次渲染都会重新执行函数体,无法直接使用普通变量来保存值。而使用
useRef
创建的 ref 对象就可以用来保存这些值。 - 通过修改
ref.current
的值,可以在组件的多次渲染之间保持变量的持久性。
- 对于函数组件而言,由于每次渲染都会重新执行函数体,无法直接使用普通变量来保存值。而使用
注意事项:
useRef
返回的ref
对象在组件重新渲染时不会触发更新,因此对其修改并不会引发组件的重新渲染。如果需要触发组件重新渲染,可以结合其他 Hook(如useState
)来实现。
为了便于理解,这里给出一个完整例子:
jsx
import React, { useRef } from 'react';
function ExampleComponent() {
const inputRef = useRef(null);
const countRef = useRef(0);
const handleClick = () => {
// 获取输入框的值
console.log(inputRef.current.value);
// 保存和修改组件内部变量
countRef.current += 1;
console.log(countRef.current);
};
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={handleClick}>点击</button>
</div>
);
}
export default ExampleComponent;
在上述示例中,我们创建了两个 useRef
对象 inputRef
和 countRef
。
inputRef
用于获取输入框的值。我们将其赋值给<input>
元素的ref
属性,这样就可以通过inputRef.current.value
获取到输入框当前的值。countRef
用于保存和修改组件内部变量。每次点击按钮时,我们通过countRef.current
将计数器加一,并打印出来。
这样,在组件重新渲染时,inputRef
和 countRef
的引用保持不变,可以在多次渲染之间共享和保留数据。
简而言之,useRef
是 React 中一个非常有用的 Hook,它能够创建一个持久存在的引用,并可用于获取和修改 DOM 节点、保存组件内部变量等场景。
forwardRef
通过使用 forwardRef
函数包装函数式组件,可以将 ref
作为参数传递给函数式组件,并在函数式组件内部通过 ref
参数来获取 ref
对象。
以下是一个示例,演示如何在 React 函数式组件中使用 forwardRef
来传递 ref
:
jsx
import React, { forwardRef } from 'react';
const TextInput = forwardRef((props, ref) => {
const handleChange = (event) => {
if (props.onChange) {
props.onChange(event.target.value);
}
};
return (
<input type="text" ref={ref} onChange={handleChange} />
);
});
export default TextInput;
下面是在父组件中使用 TextInput
组件的例子:
jsx
import React, { useRef } from 'react';
import TextInput from './TextInput';
function ExampleComponent() {
const inputRef = useRef(null);
const handleClick = () => {
console.log(inputRef.current.value);
};
return (
<div>
<TextInput ref={inputRef} onChange={(value) => console.log(value)} />
<button onClick={handleClick}>点击</button>
</div>
);
}
export default ExampleComponent;
-
注意事项:
forwardRef
只能转发单个ref
。也就是说,如果子组件内部有多个需要访问的元素或组件,只能选择其中一个进行转发。forwardRef
必须接收第二个参数ref
,并将其传递给实际的子组件。这样才能确保父组件通过ref
属性传递的引用能够正确传递给子组件内部。- 子组件接收到的
ref
参数不是常规的props
参数。在处理ref
参数时,应该遵循forwardRef
函数提供的格式。 - 当使用
forwardRef
转发ref
时,父组件传递给子组件的ref
属性将不再生效。也就是说,在父组件中通过ref
访问子组件的属性和方法时,应该直接使用传递给子组件的ref
。 - 在函数组件中使用
forwardRef
时,需要使用forwardRef
函数包装整个函数组件。如果直接导出的是函数组件本身,则无法使用ref
。
-
缺点
通常情况下,我们可以通过
forwardRef
API 将子组件内部的 DOM 元素或组件实例转发给父组件,从而实现在父组件中操作子组件。但是这种方式有一些缺点:- 转发的
ref
只能是单个元素或组件,而不能是多个; - 转发后父组件需要了解转发的元素或组件的具体实现细节等问题。
- ......
- 转发的
使用useImperativeHandle
可以克服这些缺点。
useImperativeHandle
useImperativeHandle
是一个 React Hook,用于调整由子组件向父组件暴露的 ref
对象。可以使用该 Hook 来自定义子组件实例或 DOM 元素暴露给父组件的属性和方法。它可以在子组件中控制哪些属性和方法可以被父组件访问。
通过 useImperativeHandle
Hook 返回一个对象,该对象将作为子组件向外暴露的属性和方法。这样就可以控制子组件暴露给父组件的属性和方法,并隐藏一些不必要的实现细节。
下面是一个 useImperativeHandle
的例子:
jsx
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
const childInput = forwardRef((props, ref) => {
const inputRef1 = useRef(null);
const inputRef2 = useRef(null);
useImperativeHandle(ref, () => {
return {
focusInput1: () => {
console.log('Focusing Input1...');
inputRef1.current.focus();
},
focusInput2: () => {
console.log('Focusing Input2...');
inputRef2.current.focus();
},
getValue1: () => {
return inputRef1.current.value;
},
getValue2: () => {
return inputRef2.current.value;
}
};
});
return (
<div>
<input type="text" ref={inputRef1} />
<input type="text" ref={inputRef2} />
</div>
);
});
export default childInput;
在上述代码中,我们创建了两个分别对应两个 <input>
元素的 ref
,即 inputRef1
和 inputRef2
。然后在 useImperativeHandle
中通过返回一个对象,可以将这两个 ref
都转发给父组件。
在父组件中使用转发后的 ref
来访问子组件的属性和方法,可以按照如下方式进行:
jsx
import React, { useRef } from 'react';
import childInput from './childInput';
function ParentComponent() {
const fancyInputRef = useRef(null);
const handleClick = () => {
console.log(fancyInputRef.current.getValue1());
console.log(fancyInputRef.current.getValue2());
fancyInputRef.current.focusInput1();
};
return (
<div>
<childInput ref={fancyInputRef} />
<button onClick={handleClick}>点击</button>
</div>
);
}
export default ParentComponent;
-
注意事项
- 尽量避免使用
ref
来访问组件内部的状态或属性,因为这样会破坏封装性。如果需要暴露一些方法或属性可以通过useImperativeHandle
实现。 useImperativeHandle
并不是必需的,只有当需要向外部暴露某些特定的方法和属性时才需要使用。useImperativeHandle
中返回的对象应该只包含那些必须暴露的方法和属性,尽量不要暴露不必要的方法和属性,这样可以保持接口的简单性和清晰性- 在使用
useImperativeHandle
时,父组件必须使用ref
属性引用子组件,否则无法访问子组件暴露的属性和方法。 - 子组件暴露的属性和方法应该遵循常规函数和组件操作习惯,如:命名规范、参数传递等。 不要滥用
useImperativeHandle
,因为useImperativeHandle
可能会产生一些性能问题,它会使得父组件的渲染函数在子组件变化时被调用。 - 在
useImperativeHandle
中返回的对象应该是不可变的,避免在返回对象中添加或删除属性或方法。 useImperativeHandle
需要放在组件内部使用,而不能在顶层作用域调用。
- 尽量避免使用
总结
在 React 中,ref
是一个强大的特性,提供了三种常用的方式来获取组件实例或 DOM 元素。通过字符串 ref(已经被淘汰,不推荐使用)、回调函数 ref 和创建 ref 对象,我们可以轻松地操作组件中的数据和 DOM 元素,实现更灵活的交互和操作。无论是获取组件实例、控制焦点、执行动画还是其他操作,ref 都是不可或缺的利器。因此,在开发 React 应用时,充分利用 ref 这一特性,提升代码的可读性和可维护性,为用户带来更好的体验。