在 React 组件开发中,ref 是连接组件与真实 DOM 节点或类组件实例的重要桥梁。官方提供的 createRef() 方法适合大多数基础引用场景,而自定义 Ref 对象则允许开发者在 ref 赋值过程中加入额外逻辑。两者都能完成引用管理,但设计目标、扩展能力和维护成本并不相同。选择时不能只看能否拿到节点,还要看是否需要监听赋值、保持对象稳定、兼容函数组件,以及是否会让组件行为变得复杂。

一、createRef 的基本用法与适用边界
createRef 是 React 官方提供的创建 ref 对象的方法。调用后会返回一个普通对象,其 current 属性初始为 null。当 ref 被挂载到 DOM 元素或类组件实例上后,React 会把对应节点或实例写入 current。开发者随后可以通过 current 调用 DOM API 或访问实例方法。由于它语义明确、使用简单,是类组件中获取输入框、画布、弹窗等节点时的常见选择。
import React, { createRef, Component } from 'react';
class DemoComponent extends Component {
constructor(props) {
super(props);
// 创建 ref 对象,current 初始为 null
this.inputRef = createRef();
}
handleFocus = () => {
// 组件挂载后 current 指向输入框 DOM 节点
this.inputRef.current.focus();
};
render() {
return (
<div>
<input ref={this.inputRef} type="text" />
<button onClick={this.handleFocus}>聚焦输入框</button>
</div>
);
}
}
export default DemoComponent;
从实现角度看,createRef 的核心非常轻,它只是返回一个带 current 属性的对象。理解这一点有助于判断它的边界:它本身不负责生命周期管理,也不会在赋值时执行额外逻辑,更不会帮你判断节点是否可用。真正完成赋值的是 React 的 ref 挂载机制。
function createRef() {
const refObject = {
current: null
};
return refObject;
}
使用 createRef 时,一个关键细节是对象稳定性。每次调用 createRef() 都会产生一个新对象,因此类组件通常把调用放在构造函数中,而不是 render 方法中。如果在 render 中创建,组件重新渲染时 ref 对象会变化,可能导致不必要的重新挂载或引用失效。函数组件中虽然也可以调用 createRef(),但更推荐 useRef(),因为它会在组件整个生命周期内保持同一个对象。
二、自定义 Ref 对象的实现方式与能力扩展
自定义 Ref 对象并不是另一个官方 API,而是开发者按照 ref 协议自行构造的对象。React 在挂载 ref 时,核心检查对象是否具备 current 属性,并把节点或实例写入该属性。只要对象满足这一基本约定,就可以承载额外的属性、方法或赋值监听逻辑。相比 createRef 的纯容器形态,自定义 Ref 更接近一个小型引用管理器。
下面的示例通过 getter 和 setter 扩展了 current 的赋值过程。React 仍然只负责执行赋值,但对象内部可以记录日志、缓存引用或触发后续处理。这种方式适合需要观察 ref 变化、统一处理引用赋值、或把引用状态纳入组件内部管理的场景。
import React, { Component } from 'react';
class CustomRefDemo extends Component {
constructor(props) {
super(props);
// 使用闭包保存真实值,避免直接暴露可变属性
let currentValue = null;
// React 只要求 ref 对象具备 current 属性,
// 这里通过 getter 和 setter 扩展赋值时的监听能力
this.customRef = {
get current() {
return currentValue;
},
set current(value) {
console.log('ref 被赋值:', value);
currentValue = value;
},
getAssignedValue() {
return currentValue;
}
};
}
componentDidMount() {
// 挂载完成后可以读取 ref 的当前值
console.log('当前输入框节点:', this.customRef.current);
console.log('扩展方法读取的值:', this.customRef.getAssignedValue());
}
render() {
return (
<div>
<input ref={this.customRef} type="text" />
</div>
);
}
}
export default CustomRefDemo;
回调函数形式的 ref 也可以看作一种自定义引用管理。React 会在组件挂载、更新或卸载时调用回调,并传入节点或 null。与对象 ref 相比,回调 ref 更灵活,适合在挂载瞬间执行一次性操作,例如初始化第三方库、获取尺寸、注册事件。缺点是回调函数如果每次渲染都重新创建,可能导致重复调用,因此类组件中常把回调定义为稳定方法,函数组件中可结合 useCallback 保持引用稳定。
import React, { Component } from 'react';
class CallbackRefDemo extends Component {
handleInputRef = (node) => {
// 挂载时 node 为 DOM 节点,卸载时 node 为 null
if (node) {
console.log('输入框已挂载');
node.focus();
} else {
console.log('输入框已卸载');
}
this.inputNode = node;
};
render() {
return (
<div>
<input ref={this.handleInputRef} type="text" />
</div>
);
}
}
export default CallbackRefDemo;
自定义 Ref 对象的价值在于扩展,但也要避免过度设计。如果只是为了拿一个输入框节点,额外封装 getter、setter 或闭包会增加理解成本。更稳妥的做法是:先判断 createRef 或 useRef 是否足够,只有当赋值过程确实需要副作用、日志、缓存或兼容旧框架时,才引入自定义 Ref。
三、createRef 与自定义 Ref 的核心差异对比
从工程角度看,createRef 和自定义 Ref 的差异主要体现在创建方式、扩展能力、适用场景和函数组件适配上。createRef 的优势是标准、简单、可预测;自定义 Ref 的优势是灵活,可以把引用管理从取值扩展为过程控制。
| 对比维度 | createRef | 自定义 Ref 对象 |
|---|---|---|
| 创建方式 | 调用 React 官方提供的 createRef() 方法 | 开发者自行封装对象,必须包含 current 属性 |
| 可扩展性 | 默认只暴露 current,扩展需要额外封装 | 可通过 getter、setter、额外方法扩展赋值逻辑 |
| 适用场景 | 常规获取 DOM 节点或类组件实例 | 需要监听赋值、记录日志、缓存引用、触发回调 |
| 函数组件适配 | 函数组件中可以在渲染中调用,但会每次创建新对象,通常改用 useRef | 函数组件中也可以自定义,但需自行保证引用稳定 |
在场景选择上,可以先按需求复杂度分层判断。如果目标只是稳定拿到节点或实例,优先选择官方简单方案;如果目标是在赋值过程中加入行为,再考虑自定义方案。
优先选择 createRef 的场景
- 只需要获取 DOM 节点或类组件实例,不需要额外的监听或扩展逻辑。
- 在类组件中使用
ref,并且希望写法符合官方推荐。 - 项目强调可读性和维护成本,不需要自定义
ref相关逻辑。
优先选择自定义 Ref 对象的场景
- 需要在
ref被赋值或清空时执行日志、回调、校验等额外逻辑。 - 需要给
ref对象添加额外属性或方法,扩展引用管理能力。 - 使用回调函数形式的
ref,需要更灵活的挂载和卸载处理。 - 需要兼容旧版本 React,或有特殊的上层框架集成需求。
无论选择哪种方式,都要遵守 ref 的基本使用约束。
- 普通
ref不能直接用于函数组件实例,因为函数组件没有实例;如果希望函数组件暴露内部节点,应使用forwardRef或在内部使用useRef管理。 - 不要在渲染过程中读取或写入
current,推荐在事件处理函数、生命周期方法或useEffect中访问。 - 自定义
Ref必须保留current属性,否则 React 无法完成挂载赋值。 - 函数组件中如果需要使用类似
createRef的能力,官方推荐使用useRef,它返回的对象在组件生命周期内保持稳定。
函数组件中使用 useRef 时,通常把 ref 的读取放在 useEffect 中,这样可以避免在渲染阶段访问 DOM,也符合副作用执行时机。
import React, { useRef, useEffect } from 'react';
function FunctionRefDemo() {
// useRef 返回的对象在组件生命周期内保持稳定
const inputRef = useRef(null);
useEffect(() => {
// 在副作用中读取 ref,避免渲染阶段访问 DOM
inputRef.current.focus();
}, []);
return (
<div>
<input ref={inputRef} type="text" />
</div>
);
}
export default FunctionRefDemo;
四、总结与延伸建议
综合来看,createRef 是基础且稳妥的选择,适合只需要稳定引用的场景;自定义 Ref 对象是扩展型选择,适合需要在引用赋值过程中加入额外行为的场景。实际项目中,应优先使用官方提供的简单方案,只有在明确需求驱动下才封装自定义 Ref。
延伸建议是:在引入自定义 Ref 前,先明确它要解决的问题,例如日志、缓存、回调、兼容旧框架或测试替换;其次保证 ref 对象在组件生命周期内稳定;最后把 ref 的读写放在副作用或事件处理中,避免渲染阶段访问 DOM。先让 ref 保持最小可用,再按需求扩展,这样既能保持组件行为可预测,也能在需要扩展时拥有足够空间。