一、先看个场景:你是不是也遇到过这种坑
假设你写了个超简单的输入框组件:
// 子组件:MyInput.jsx
const MyInput = (props) => {
return <input {...props} />
}
// 父组件:App.jsx
function App() {
const inputRef = React.useRef(null)
// 想在组件挂载后自动聚焦输入框
React.useEffect(() => {
inputRef.current.focus() // 这里会报错!因为inputRef.current是null 😭
}, [])
return <MyInput ref={inputRef} />
}
运行之后控制台直接红了 ——"Cannot read properties of null (reading 'focus')"。这时候你可能会挠头:明明给子组件传了 ref,怎么就是拿不到 DOM 元素?
别急!这不是你的错,而是 React 的 "安全机制"——默认情况下,函数组件就像个害羞的小姑娘,会把 ref 拒之门外。这时候就该forwardRef
登场了,它就像个贴心的快递员,能帮你把 ref 稳稳地送到子组件内部 📦
二、先搞懂:React 里的 ref 到底是啥?
在讲forwardRef
之前,得先明白ref
的基础用法。你可以把ref
理解成 "DOM 元素的身份证"—— 有了它,你就能直接找到并操作对应的 DOM 元素。
(1)ref 能做啥?
- 让输入框自动聚焦(就像登录页点进去自动光标闪烁)
- 获取输入框的实时值(不用通过 onChange 层层传递)
- 操作 DOM 的样式或属性(比如滚动到指定位置)
- 调用子组件的方法(比如让弹窗组件关闭)
(2)基础用法(给 DOM 元素挂 ref)
function App() {
// 1. 创建一个ref容器
const inputRef = React.useRef(null)
// 3. 在合适的时机使用ref
const handleFocus = () => {
inputRef.current.focus() // 调用DOM原生方法
console.log('当前输入值:', inputRef.current.value)
}
// 2. 把ref挂到DOM元素上
return (
<div>
<input ref={inputRef} placeholder="点击按钮聚焦我" />
<button onClick={handleFocus}>点我聚焦</button>
</div>
)
}
这种直接给 DOM 元素挂 ref 的方式很简单,但如果这个 input 被封装成了组件,问题就来了 —— 刚才的例子已经证明:函数组件默认不接收 ref。
三、forwardRef:给函数组件开个 "ref 通道"
forwardRef
(中文可以叫 "转发 ref")是 React 提供的高阶组件,它的核心作用就一个:让函数组件能接收 ref,并把 ref 转发给内部的 DOM 元素。
(1)基本用法(解决开头的坑)
import React, { forwardRef } from 'react'
// 用forwardRef包裹函数组件,就能接收ref参数了
const MyInput = forwardRef((props, ref) => {
// 把接收到的ref转发给内部的input元素
return <input {...props} ref={ref} />
})
function App() {
const inputRef = React.useRef(null)
React.useEffect(() => {
inputRef.current.focus() // 现在能正常聚焦了!🎉
}, [])
return <MyInput ref={inputRef} placeholder="我能自动聚焦啦" />
}
这段代码的关键变化:
- 子组件用
forwardRef
包裹后,参数变成了(props, ref)
(注意第二个参数才是 ref) - 子组件内部把 ref 绑到了真正的 DOM 元素上(这里是 input)
- 父组件的 ref 最终指向的是子组件内部的 input,而不是子组件本身
(2)为什么需要 forwardRef?
React 这么设计是有原因的:函数组件本身没有实例(不像类组件有 this),如果直接给函数组件传 ref,React 也不知道该指向哪里。而forwardRef
相当于告诉 React:"你把 ref 传给我,我知道该给谁"。
举个生活例子:你想给朋友的孩子送礼物(ref),但你只认识朋友(子组件),不认识孩子(内部 DOM)。这时候forwardRef
就像朋友说:"把礼物给我,我转交给孩子"。
四、哪些场景必须用 forwardRef?
不是所有组件都需要forwardRef
,这几种情况一定要用:
(1)需要父组件操作子组件内部 DOM
最常见的就是 "自动聚焦" 需求,比如:
-
登录页加载完成后,光标自动定位到用户名输入框
-
弹窗打开后,输入框自动获得焦点
-
表单验证失败时,自动聚焦到错误的输入框
// 带自动聚焦的输入框组件
const FocusableInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />
})
// 使用组件的页面
function LoginPage() {
const usernameRef = React.useRef(null)
// 页面加载完成后聚焦用户名输入框
React.useEffect(() => {
usernameRef.current.focus()
}, [])
return (
<div>
<FocusableInput ref={usernameRef} placeholder="用户名" />
<FocusableInput placeholder="密码" type="password" />
</div>
)
}
(2)封装通用组件时暴露内部 DOM
如果你封装了一个 UI 组件库(比如自定义下拉框、日期选择器),用户可能需要通过 ref 获取内部 DOM 的位置或尺寸。这时候用forwardRef
把 ref 转发到关键 DOM 上,会让组件更易用。
// 封装一个带图标的输入框组件
const IconInput = forwardRef(({ icon, ...props }, ref) => {
return (
<div style={{ display: 'flex', alignItems: 'center' }}>
<span>{icon}</span>
{/* 把ref转发给实际输入框,方便外部操作 */}
<input {...props} ref={ref} style={{ marginLeft: 8 }} />
</div>
)
})
// 使用时能直接操作内部input
function SearchBar() {
const inputRef = React.useRef(null)
const handleSearch = () => {
const value = inputRef.current.value
console.log('搜索内容:', value)
}
return (
<div>
<IconInput
ref={inputRef}
icon="🔍"
placeholder="输入关键词搜索"
/>
<button onClick={handleSearch}>搜索</button>
</div>
)
}
五、这些坑千万别踩!(注意事项)
(1)函数组件必须用 forwardRef 才能接 ref
这是新手最容易犯的错!直接在函数组件上用 ref 会被忽略:
// 错误示例:没包forwardRef,ref会无效
const BadInput = (props) => {
return <input {...props} />
}
function App() {
const ref = React.useRef(null)
return <BadInput ref={ref} /> // 这里的ref不会生效!⚠️
}
(2)参数顺序不能错
被forwardRef
包裹的组件,第一个参数是props
,第二个才是ref
,不能搞反:
// 正确写法
const GoodComponent = forwardRef((props, ref) => { ... })
// 错误写法(ref位置错了)
const BadComponent = forwardRef((ref, props) => { ... }) // ❌
(3)ref 最终指向你绑定的元素
很多人以为 ref 会指向子组件本身,其实不是 ——ref 指向的是你在子组件里最终绑定的那个元素:
const MyComponent = forwardRef((props, ref) => {
return (
<div>
{/* ref绑给了p标签,所以父组件拿到的就是p标签 */}
<p ref={ref}>我是被ref绑定的元素</p>
<input placeholder="我没被绑定,拿不到哦" />
</div>
)
})
(4)类组件不需要 forwardRef
forwardRef
只给函数组件用,类组件本身就能接收 ref:
// 类组件直接能用ref,不用forwardRef
class ClassComponent extends React.Component {
render() {
return <input />
}
}
function App() {
const ref = React.useRef(null)
return <ClassComponent ref={ref} /> // 完全没问题
}
六、结合项目代码深入理解
我们来看一个实际项目中的例子(就是你提供的代码):
import React, { forwardRef, useRef, useEffect } from 'react'
// 定义子组件
function Guang(props, ref) {
return (
<div>
{/* 把ref绑到input上 */}
<input type="text" ref={ref} />
</div>
)
}
// 用forwardRef包装,让它能接收ref
const WrappedGuang = forwardRef(Guang);
// 父组件
function App() {
// 创建ref
const inputRef = useRef(null);
// 组件挂载后让input自动聚焦
useEffect(() => {
inputRef.current?.focus(); // 这里的current就是子组件里的input
}, []);
// 给子组件传ref
return <WrappedGuang title="Xiang" ref={inputRef} />;
}
这段代码的执行流程就像这样:
-
父组件创建
inputRef
→ 传给WrappedGuang
组件 -
forwardRef
把inputRef
转发给Guang
组件的第二个参数 -
Guang
组件把ref
绑到内部的input
元素上 -
父组件通过
inputRef.current
就能操作这个input
了
如果没有forwardRef
会怎样?——inputRef.current
会是undefined
,因为函数组件默认不接收 ref。就像快递员(ref)到了小区门口(子组件),发现没人接,只能原路返回。
七、进阶:配合 useImperativeHandle 自定义暴露内容
有时候你不想让父组件直接操作 DOM,而是想暴露特定方法(比如 "清空输入框"、"验证内容"),这时候可以用useImperativeHandle
配合forwardRef
。
它的作用是:自定义通过 ref 暴露给父组件的内容(默认暴露的是整个 DOM 元素)。
import React, { forwardRef, useRef, useImperativeHandle } from 'react'
const CustomInput = forwardRef((props, ref) => {
// 内部真实的DOM ref
const inputRef = useRef(null)
// 自定义暴露给父组件的方法
useImperativeHandle(ref, () => ({
// 只暴露需要的方法,而不是整个DOM
focus: () => {
inputRef.current.focus()
},
clear: () => {
inputRef.current.value = ''
},
// 甚至可以返回处理后的值
getValue: () => {
return inputRef.current.value.trim() // 自动去除空格
}
}))
return <input {...props} ref={inputRef} />
})
// 父组件使用
function App() {
const inputRef = useRef(null)
return (
<div>
<CustomInput placeholder="试试下面的按钮" />
<button onClick={() => inputRef.current.focus()}>聚焦</button>
<button onClick={() => inputRef.current.clear()}>清空</button>
<button onClick={() => alert(inputRef.current.getValue())}>获取值</button>
</div>
)
}
这样做的好处是:
- 隐藏内部实现细节(父组件不用知道你用的是 input 还是 textarea)
- 控制暴露范围(避免父组件乱改 DOM 属性)
- 提供更友好的 API(比如 getValue 自动处理空格)
八、总结:forwardRef 一句话总结
forwardRef
就像 React 组件间的 "ref 快递中转站"—— 让父组件的 ref 能穿过函数组件,准确送到内部的 DOM 元素手上。当你需要在父组件操作子组件内部 DOM 时,它就是最佳选择。
最后再给新手朋友一个小建议:刚开始用的时候可以多打印ref.current
看看里面是什么(console.log(ref.current)
),直观感受 ref 的指向变化。用多了就会发现,这东西其实一点也不难!😉
如果觉得有收获,欢迎点赞收藏~有问题可以在评论区交流哦!