React 函数组件中获取 ref 及其组件方法详解
在 React 开发中,ref 是一个非常实用的特性,它允许我们直接访问 DOM 元素或组件实例。对于函数组件而言,由于其没有实例,获取 ref 及组件方法的方式与类组件有所不同。本文将详细介绍在 React 函数组件中如何拿到 ref 以及组件方法,帮助开发者更好地掌握这一知识点。
一、ref 的基本概念
在 React 中,ref 是一个特殊的属性,它允许我们访问 DOM 元素或者 React 组件实例。通常情况下,我们通过 props 和 state 来管理组件的交互,但有时候需要直接操作 DOM 或组件实例,比如获取输入框的值、聚焦输入框、滚动到某个元素等,这时候 ref 就派上用场了。
ref 的作用主要体现在以下几个方面:
-
访问 DOM 元素:可以直接获取 DOM 元素的属性和方法,进行 DOM 操作,如修改样式、获取尺寸等。
-
访问组件实例:对于类组件,可以获取组件实例,从而调用组件内部的方法。
-
管理非受控组件:在非受控组件中,使用 ref 来获取表单元素的值。
二、类组件中 ref 的使用
在了解函数组件中 ref 的使用之前,先来看一下类组件中如何使用 ref,以便进行对比。
2.1 使用 createRef
类组件中可以通过React.createRef()
来创建 ref,然后将其赋值给组件的属性,从而获取 DOM 元素或组件实例。
import React, { Component, createRef } from 'react';
class MyInput extends Component {
constructor(props) {
super(props);
this.inputRef = createRef();
}
focusInput = () => {
this.inputRef.current.focus();
};
render() {
return <input ref={this.inputRef} />;
}
}
class ParentComponent extends Component {
constructor(props) {
super(props);
this.myInputRef = createRef();
}
handleClick = () => {
this.myInputRef.current.focusInput();
};
render() {
return (
<div>
<MyInput ref={this.myInputRef} />
<button onClick={this.handleClick}>聚焦输入框</button>
</div>
);
}
}
在上面的例子中,MyInput
组件是一个类组件,它通过createRef
创建了inputRef
来获取输入框的 DOM 元素,并定义了focusInput
方法来聚焦输入框。ParentComponent
组件通过createRef
创建myInputRef
并赋值给MyInput
组件的ref
属性,从而可以调用MyInput
组件的focusInput
方法。
2.2 使用 findDOMNode
除了createRef
,还可以使用ReactDOM.findDOMNode
来获取 DOM 元素,但这种方式不推荐使用,因为它可能会导致一些问题,比如在组件未挂载时调用会报错。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class MyComponent extends Component {
componentDidMount() {
const domNode = ReactDOM.findDOMNode(this);
console.log(domNode);
}
render() {
return <div>Hello World</div>;
}
}
三、函数组件中获取 ref
函数组件没有实例,所以不能像类组件那样直接使用 ref 来获取组件实例。但我们可以通过一些方法来获取函数组件内部的 DOM 元素或让函数组件暴露方法给父组件。
3.1 获取函数组件内部的 DOM 元素
在函数组件中,可以使用useRef
钩子来创建 ref,然后将其赋值给 DOM 元素的ref
属性,从而获取 DOM 元素。
import React, { useRef, useEffect } from 'react';
function MyInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}
在这个例子中,MyInput
是一个函数组件,通过useRef
创建了inputRef
,并将其赋值给输入框的ref
属性。在useEffect
钩子中,我们可以通过inputRef.current
访问到输入框的 DOM 元素,并调用focus
方法使其自动聚焦。
3.2 通过 forwardRef 获取函数组件的 ref
当父组件需要获取函数组件内部的 DOM 元素时,由于函数组件没有实例,直接使用ref
属性是不行的。这时候可以使用React.forwardRef
来将 ref 转发到函数组件内部的 DOM 元素上。
import React, { useRef, forwardRef } from 'react';
// 使用forwardRef转发ref
const MyInput = forwardRef((props, ref) => {
return <input ref={ref} />;
});
function ParentComponent() {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus();
};
return (
<div>
<MyInput ref={inputRef} />
<button onClick={handleClick}>聚焦输入框</button>
</div>
);
}
在上面的代码中,MyInput
函数组件通过forwardRef
进行了包装,forwardRef
接受一个函数作为参数,该函数接收props
和ref
两个参数,并将ref
传递给了内部的input
元素。这样,ParentComponent
组件就可以通过ref
获取到MyInput
组件内部的input
元素,并调用其focus
方法。
四、在函数组件中获取组件的方法
有时候,父组件需要调用子组件(函数组件)中的方法。这时候,我们可以结合useImperativeHandle
和forwardRef
来实现。
useImperativeHandle
钩子可以自定义暴露给父组件的实例值,即父组件通过ref
只能访问到我们允许的方法或属性。
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
const ChildComponent = forwardRef((props, ref) => {
const [count, setCount] = React.useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
// 自定义暴露给父组件的方法
useImperativeHandle(ref, () => ({
increment,
decrement,
getCount: () => count
}));
return <div>当前计数:{count}</div>
});
function ParentComponent() {
const childRef = useRef(null);
const handleIncrement = () => {
childRef.current.increment();
};
const handleDecrement = () => {
childRef.current.decrement();
};
const handleGetCount = () => {
console.log('当前计数:', childRef.current.getCount());
};
return (
<div>
<ChildComponent ref={childRef} />
<button onClick={handleIncrement}>增加</button>
<button onClick={handleDecrement}>减少</button>
<button onClick={handleGetCount}>获取计数</button>
</div>
);
}
在这个例子中,ChildComponent
是一个函数组件,它内部定义了increment
、decrement
方法和count
状态。通过useImperativeHandle
,我们将increment
、decrement
和getCount
方法暴露给了父组件。ParentComponent
组件通过ref
获取到ChildComponent
暴露的方法,并可以调用这些方法来操作子组件的状态。
五、注意事项
-
避免过度使用 ref:React 鼓励通过 props 和 state 来管理组件的交互和状态,过度使用 ref 会使组件之间的耦合度增加,不利于代码的维护和复用。只有在必要的时候,比如需要直接操作 DOM 或调用子组件的特定方法时,才使用 ref。
-
forwardRef
只能用于函数组件:forwardRef
是专门为函数组件设计的,用于转发 ref。类组件本身可以直接接收 ref,不需要使用forwardRef
。 -
useImperativeHandle
应配合forwardRef
使用:useImperativeHandle
用于自定义暴露给父组件的实例值,它必须与forwardRef
一起使用,否则ref
参数将为undefined
。 -
ref 的.current 属性:在使用 ref 时,要注意
ref.current
的值在不同阶段的变化。在组件挂载前,ref.current
为null
;组件挂载后,ref.current
才会指向 DOM 元素或组件实例(对于类组件)。 -
不要在函数组件内部的普通 JavaScript 函数中使用 ref:因为 ref 的更新是异步的,在普通函数中可能无法获取到最新的 ref 值。如果需要在函数中使用 ref,应该将其放在
useEffect
钩子中。