在 React 中,ref是个很实用的东西 —— 它能让我们直接访问 DOM 元素或组件实例,比如给输入框自动聚焦、获取元素尺寸等。但用着用着你会发现:类组件可以轻松接收ref,可函数组件却不行?每次尝试传递ref,控制台要么报错,要么ref.current是undefined。
别慌,这不是函数组件的 “bug”,而是 React 的设计规范。今天就用一个实际例子,聊聊为什么函数组件不能直接接收ref,以及forwardRef是如何像 “传话筒” 一样解决这个问题的。
函数组件直接传 ref
// 子组件:一个简单的函数组件,想接收ref
function Guang(props, ref) {
// 尝试打印ref,看看能不能拿到
console.log("子组件的ref:", ref);
return (
<div>
<input type="text" ref={ref} /> {/* 想把ref绑定到input上 */}
</div>
);
}
// 父组件:想通过ref控制子组件的input
function App() {
const inputRef = useRef(null);
// 组件挂载后,尝试让input自动聚焦
useEffect(() => {
inputRef.current?.focus(); // 这里会报错吗?
}, []);
return (
<div>
{/* 直接给函数组件传ref */}
<Guang ref={inputRef} />
</div>
);
}
- 父组件的
useEffect里,inputRef.current也是undefined,调用focus()会报错。
这说明:函数组件默认不能接收ref参数,父组件传递的ref根本传不进去。
为什么函数组件不能直接接收 ref?
React 这么设计,其实是为了避免 “滥用 ref”。
函数组件的核心是 “无状态”(早期 React 函数组件没有state),它更像是一个 “渲染函数”—— 接收props,返回 JSX。而ref的作用是 “访问实例”,但函数组件本身没有实例(不像类组件有this),React 担心开发者会试图通过ref访问函数组件内部的状态或方法,这会破坏 React “单向数据流” 的设计理念(数据应该通过props传递,而不是直接操作组件内部)。
打个比方:函数组件就像一台自动售货机(只接受硬币props,吐出商品 JSX),而ref就像一把钥匙,React 不允许你直接用钥匙打开机器内部(避免乱改内部结构)。
forwardRef:给函数组件装个 “传话筒”
如果我们确实需要访问函数组件内部的 DOM 元素(比如让子组件的input自动聚焦),该怎么办?
React 提供了forwardRef—— 它的作用就像一个 “传话筒”,能把父组件的ref穿过函数组件,传递到组件内部的 DOM 元素上。
用法步骤:3 步搞定 ref 转发
步骤 1:用 forwardRef 包裹子组件
// 用forwardRef包裹函数组件,让它能接收ref
const WrapperGuang = forwardRef(Guang);
forwardRef接收一个函数组件作为参数,返回一个 “能接收 ref 的新组件”。
步骤 2:子组件接收 ref 参数
函数组件需要额外接收一个ref参数(作为第二个参数):
// 子组件:第一个参数是props,第二个参数是ref
function Guang(props, ref) {
console.log("子组件收到的ref:", ref); // 这次能拿到ref了!
return (
<div>
{/* 把ref绑定到内部的input上 */}
<input type="text" ref={ref} />
</div>
);
}
步骤 3:父组件正常传递 ref
function App() {
const inputRef = useRef(null);
useEffect(() => {
// 现在能拿到input的DOM节点了!
inputRef.current?.focus(); // 成功自动聚焦
}, []);
return (
<div>
{/* 给forwardRef包装后的组件传ref */}
<WrapperGuang ref={inputRef} />
</div>
);
}
- 父组件的
inputRef.current指向input的 DOM 节点,调用focus()成功让输入框自动聚焦。
原理:forwardRef 是如何 “转发” ref 的?
forwardRef的工作流程可以简单理解为:
- 父组件创建
ref对象(inputRef = useRef(null)); - 父组件将
ref传递给forwardRef包装后的组件(WrapperGuang); forwardRef把这个ref作为第二个参数,传递给原始函数组件(Guang);- 原始函数组件将
ref绑定到内部的 DOM 元素(input)上; - 最终,父组件的
inputRef.current指向这个input的 DOM 节点。
就像一个 “传话筒”:父组件把 “话”(ref)递给传话筒(forwardRef),传话筒再把 “话” 传给子组件,子组件听到后执行操作(绑定到 input)。
注意事项:这些细节别踩坑
-
ref 是第二个参数,不能写在 props 里
函数组件接收ref时,必须作为第二个参数,不能从props里解构(props.ref是拿不到的):// 错误写法:ref不在props里 function Guang(props) { const { ref } = props; // 这样拿不到ref! return <input ref={ref} />; } // 正确写法:ref是第二个参数 function Guang(props, ref) { // 单独接收ref return <input ref={ref} />; } -
forwardRef 不影响 props 的传递
forwardRef只负责转发ref,不会干扰props的传递。父组件传递的其他属性,依然通过第一个参数接收:// 父组件传递props <WrapperGuang ref={inputRef} name="用户名" /> // 子组件接收props和ref function Guang(props, ref) { console.log("props:", props.name); // 打印"用户名" return <input ref={ref} placeholder={props.name} />; } -
类组件不需要 forwardRef
forwardRef只用于函数组件。类组件本身可以直接接收ref(指向类实例),无需转发:// 类组件可以直接接收ref class ClassComponent extends React.Component { render() { return <input ref={this.props.innerRef} />; } }
什么时候需要用 forwardRef?
forwardRef的典型使用场景是:封装通用 UI 组件时,允许使用者访问组件内部的 DOM 元素。
比如你封装了一个CustomInput组件,内部是一个input标签,其他开发者使用时可能需要聚焦、清空内容等操作。这时用forwardRef转发ref,能让使用者像操作原生input一样操作你的组件:
// 封装一个自定义输入框组件
const CustomInput = forwardRef((props, ref) => {
return (
<div className="custom-input">
<input type="text" ref={ref} {...props} />
</div>
);
});
// 其他开发者使用时,能通过ref控制内部input
function User() {
const inputRef = useRef(null);
const handleClear = () => {
inputRef.current.value = ""; // 清空输入框
};
return (
<div>
<CustomInput ref={inputRef} placeholder="请输入" />
<button onClick={handleClear}>清空</button>
</div>
);
}
总结:forwardRef 的核心价值
forwardRef解决了函数组件无法接收ref的问题,让我们既能保持函数组件的简洁性,又能灵活访问其内部的 DOM 元素。它的核心价值是:在不破坏函数组件设计理念的前提下,安全地转发 ref,满足访问内部 DOM 的需求。
记住一句话:当你需要让父组件访问函数组件内部的 DOM 元素时,forwardRef就是那个可靠的 “传话筒”。
最后,再看一眼完整的正确示例,巩固一下用法吧:
import { useRef, useEffect, forwardRef } from "react";
// 1. 定义原始函数组件,接收props和ref
function Guang(props, ref) {
return <input type="text" ref={ref} />;
}
// 2. 用forwardRef包装,得到可接收ref的组件
const WrapperGuang = forwardRef(Guang);
// 3. 父组件使用
function App() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus(); // 成功聚焦
}, []);
return <WrapperGuang ref={inputRef} />;
}
export default App;
下次封装函数组件时,别再为 “拿不到 ref” 发愁了 ——forwardRef这把 “钥匙”,能帮你打开函数组件内部 DOM 的大门~ 🚪