先记住这个答案
当逻辑直接依赖某个 DOM 节点出现、替换或移除时,回调 ref 能把动作放到节点关联过程,而不必把它误写成只在整个组件首次挂载执行的 Effect。对象 ref 主要提供可变的 current 容器,修改它不会自动触发渲染或 Effect。回调 ref 常用于条件节点聚焦、节点注册和绑定外部资源;需要清理时按 React 版本使用对应契约。回调函数身份也很重要,身份改变会引起旧关联清理与新关联建立,不能假设只有真实 DOM 被替换才会重新运行。
- 节点出现与组件首次挂载不是同一个事件
- 对象 ref 的 current 变化不会自动触发 Effect
- 稳定回调身份能减少不必要的关联重建
条件节点可能晚于父组件首次挂载
面板最初隐藏输入框,用户点击之后才渲染它。如果在父组件的空依赖 Effect 中读取对象 ref,首次执行时可能根本没有输入框;后来 ref.current 变成节点也不会自动使这个 Effect 再运行。
下面让输入框自己通过稳定回调完成聚焦,显示和隐藏都对应明确的节点关联过程。额外的“更新说明”按钮只触发同组件重渲染,节点与回调身份保持不变时,不应因为这个无关动作重新抢走焦点。
import { useCallback, useState } from 'react';
export default function ConditionalInput() {
const [visible, setVisible] = useState(false);
const [revision, setRevision] = useState(0);
const attach = useCallback((node: HTMLInputElement | null) => {
if (node) node.focus();
}, []);
return <>
<button onClick={() => setVisible(value => !value)}>切换输入</button>
<button onClick={() => setRevision(value => value + 1)}>更新说明</button>
<output>说明版本:{revision}</output>
{visible && <input ref={attach} aria-label="检索词" />}
</>;
}显示输入框时回调获得节点并聚焦,隐藏后节点被移除,再次显示会建立新的关联。更新无关的说明版本不会主动更换回调身份,因此不会仅因该次重渲染再次执行聚焦。
回调身份变化本身也会引起重新关联
每次渲染创建一个新的内联 ref 函数,会让 React 清理旧回调再调用新回调,即使 DOM 节点还是同一个。若回调负责测量、观察或聚焦,这种额外工作可能造成重复注册或用户输入时焦点跳动。
是否用 useCallback 取决于希望哪些输入变化触发重新关联。回调使用了响应式配置时不能为了身份稳定漏写依赖;应让变化对应必要的清理和重建,或者调整资源接口,使配置更新与节点登记承担不同责任。
动态列表还需要正确维护节点集合
多个条目需要按业务标识查找 DOM 时,可以在事件或回调 ref 中维护一个节点映射,并在条目移除时删除记录。不能在循环中为每个元素直接调用数量不定的 useRef,也不能只登记新增节点而永远不移除。
节点操作属于命令式接口,普通界面状态仍应由 props 和 state 表达。不要通过 ref 直接改写 React 同时管理的子树结构;仅仅获得一个节点引用,不代表可以绕过组件的数据流任意插入或删除它的子节点。
容易答错的地方
- 把 ref.current 放进依赖数组就自动监听节点变化
- current 赋值不会通知 React 渲染,依赖比较也不会独立运行;节点出现时的动作应使用正确的关联或生命周期机制。
- 回调 ref 只有 DOM 真实替换时才会运行
- 回调函数身份变化和开发检查也会影响调用,应正确实现清理并控制必要身份;不能依据节点地址相同就断言回调不会再次执行。
面试官还会怎么问?
每个回调 ref 都必须使用 useCallback 吗?
不必须,但建立资源、测量或聚焦成本较高时,稳定身份往往更清楚。若依赖确实变化需要重新关联,应保留正确依赖而不是强行空数组。
回调 ref 中可以设置 state 吗?
可以在确实需要把节点信息转换为界面状态时这样做,但要避免每次关联都制造无意义新对象,引发额外渲染和再次关联的循环。
为什么动态列表里的节点映射要按业务 id 保存?
稳定标识能在排序后仍对应同一条目,移除时也能准确清理。数组位置会随插入删除变化,容易让后续滚动或聚焦命中错误节点。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。