Skip to main content

React forwardRef 和 ref 区别是什么?displayName 又有什么作用?

· 6 min read

React ref 和 forwardRef 不是一个层级的概念。

ref 是 React 故意不放入 props 的"逃生通道",forwardRef 是把这个通道从 class 实例延伸到函数组件的桥梁。

  • ref 是 React 特殊处理的 prop:class 组件默认挂到实例,函数组件默认拿不到
  • forwardRef 把 ref 这个逃生通道"打通"到函数组件内部的 DOM 或组件实例
  • 经典组合:forwardRef + useImperativeHandle,自定义暴露给父组件的方法集合
  • displayName 给 DevTools 留个可读名字,避免看到一堆 "ForwardRef" / "Anonymous"
  • React 19 起 ref 已经是普通 prop,forwardRef 即将退役——但理解它仍是排查老代码的基础

ref 为什么是个特殊 prop

写 React 时,每个 props 都可以在组件里通过参数拿到,唯独 ref 拿不到。这是 React 故意设计的,不是 bug。

ref 在 React 里被定义为"逃生通道"——让你能绕过 props 这套声明式数据流,直接拿到组件实例或 DOM 节点。比如:

function Parent() {
const inputRef = useRef(null);

useEffect(() => {
inputRef.current?.focus(); // 直接命令式操作 DOM
}, []);

return <Child ref={inputRef} />; // ref 不在 props 里
}

class 组件里,ref 默认会挂到实例上,所以你能直接拿到 this.inputRef.current。但在函数组件里,函数没有 this,也没有实例——所以 ref 默认根本拿不到。

React 文档明确说:ref 不是 prop。React 在调和(reconciliation)阶段会单独处理 ref,不会把它放进 props 对象里传给你的组件。

forwardRef:把 ref "打通"进函数组件

forwardRef 的作用只有一个——让函数组件能接住父组件传过来的 ref。

const FancyInput = React.forwardRef((props, ref) => {
return <input ref={ref} className="fancy" {...props} />;
});

签名里 (props, ref) 是关键:第二个参数就是从父组件传过来的 ref。拿到 ref 之后,把它挂到你想要控制的 DOM 或子组件上。

典型的应用场景:

const FormInput = forwardRef(({ label, ...rest }, ref) => {
return (
<label>
{label}
<input ref={ref} {...rest} />
</label>
);
});

// 父组件
function Form() {
const inputRef = useRef(null);
return <FormInput label="用户名" ref={inputRef} />;
}

父组件通过 FormInput 这层封装,依然能直接拿到原生 input 节点的引用。

forwardRef 不能单独用。它只是"接住"父组件传过来的 ref,至于把这个 ref 挂到哪里、对外暴露什么 API,还得靠 useImperativeHandle 来定制。

useImperativeHandle:定制对外暴露的方法

默认 forwardRef 把 ref 转发到内部 DOM,父组件拿到的是完整的 DOM 节点。但在组件库里经常希望只暴露几个方法,避免父组件乱搞内部细节。

这时就用到 useImperativeHandle

const Counter = forwardRef((props, ref) => {
const [count, setCount] = useState(0);

useImperativeHandle(ref, () => ({
increment: () => setCount((c) => c + 1),
reset: () => setCount(0),
}), []);

return <div>{count}</div>;
});

// 父组件
function App() {
const counterRef = useRef(null);
return (
<>
<Counter ref={counterRef} />
<button onClick={() => counterRef.current?.increment()}>+1</button>
</>
);
}

父组件只能调用 incrementreset,不能直接 setState、不能读内部 state。这是封装组件库的关键写法。

displayName:给 DevTools 留个名字

写 forwardRef 时,你大概率在 React DevTools 里见过这样的画面:

▶ ForwardRef
▶ ForwardRef
▶ ForwardRef

一堆 "ForwardRef",根本分不清哪个是哪个。debug 时想找某个组件,全靠肉眼数位置——非常痛苦。

displayName 就是用来解决这个问题。它给组件一个人类可读的名字,让 DevTools、报错堆栈、React 警告都能正确显示组件身份。

const FancyButton = React.forwardRef(function FancyButton(props, ref) {
return <button ref={ref} {...props}>Click me</button>;
});

FancyButton.displayName = 'FancyButton';

或者更常见的写法是用完后单独赋值:

const FancyInput = React.forwardRef((props, ref) => {
// ... 组件实现
});
FancyInput.displayName = 'FancyInput'; // 显式标注

实际开发中你见过这种写法:

Button.displayName = 'Button';

它通常出现在组件库里——通过高阶函数、styled-componentsemotion 等包了一层之后,组件本身的名字变成了 "Anonymous" 或者父级组件名,必须手动把 displayName 标注回去。否则 DevTools 里全是 "Anonymous"。

displayName 不影响组件功能。它纯粹是为了调试可读性——不写组件照样能跑,但排查问题时会很难受。

几个容易踩的坑

坑 1:函数组件里直接拿 ref

function MyInput({ ref }) { // ❌ 拿不到 ref
return <input ref={ref} />;
}

ref 不会出现在 props 里。要么用 forwardRef,要么升级到 React 19 直接拿(见下节)。

坑 2:forwardRef 配 memo

export default React.memo(React.forwardRef(MyComponent));

这种组合很常见,但 React.memo 比较的是 props,不会管 ref——因为 ref 本来就不在 props 里。如果父组件用新的 ref 对象(每次渲染 useRef 是稳定的,但回调 ref 每次都是新函数),就可能绕过 memo 的优化。

坑 3:高阶组件里的 displayName

function withLoading(Component) {
function Wrapped(props) {
return <Component {...props} />;
}
Wrapped.displayName = `withLoading(${Component.displayName || Component.name || 'Component'})`;
return Wrapped;
}

这种写法不是炫技,而是 DevTools 排错的硬需求。不写 displayName,DevTools 里只会显示 "Wrapped",调试时全靠猜。

React 19 的演进:ref 已经是普通 prop 了

如果你维护的项目还在用 React 16/17/18,那 forwardRef 是必学的。但如果升级到 React 19,它已经不再需要:

// React 19:ref 就是普通 prop
function MyInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}

React 团队把 ref 提升成普通 prop 的同时,官方明确建议新代码不要再用 forwardRef——它的存在只是为了兼容历史代码。

但理解 forwardRef 的设计思路仍然有价值:

  • 为什么 ref 不在 props 里?因为它是逃生通道,绕过声明式数据流
  • 为什么需要 forwardRef?因为函数组件没有实例,ref 不知道挂哪
  • displayName 解决了什么?调试可读性——这一条即使 React 19 也仍然适用

小结

  • ref:React 故意不放入 props 的特殊 prop,class 组件挂到实例,函数组件默认拿不到
  • forwardRef:把 ref 这个逃生通道从父组件"打通"到函数组件内部
  • useImperativeHandle:在 forwardRef 基础上定制对外暴露的方法集合
  • displayName:让 DevTools 显示可读名字,避免一堆 "ForwardRef" / "Anonymous"
  • React 19 起 ref 已经是普通 prop,新代码不要再用 forwardRef

References

  1. Forwarding Refs —— React 官方文档
  2. useImperativeHandle —— React 官方文档
  3. Display Name for Debugging —— React 官方文档