Why You Need forwardRef and useImperativeHandle
React’s one-way data flow is a good thing, but it has boundaries. Understanding these boundaries is key to truly mastering ref.
How do you focus an input when a table cell is clicked? How do you directly call a child component’s validation method on form submit? These are common frontend scenarios with similar behavior: Actively triggering a child component’s internal method from the parent. Props and callbacks can’t do this, which is where forwardRef and useImperativeHandle come in.
One-way data flow
React’s core principle is one-way data flow:
- Parent to child data: props
- Child to parent data: callback functions
// 父组件给子组件传数据
<ChildComponent data={someData} />
// 子组件给父组件传数据
<ChildComponent onDataChange={handleDataChange} />
The key here is that whether it’s props or callbacks, the direction is fixed. Props are the parent “pushing” data to the child; callbacks are the child “sending” data back to the parent.
A parent component cannot directly call a child component’s methods.
Props are fundamentally configuration, not commands. The parent tells the child “what data to render”, not “what to do”.
ref
React provides ref. Ref is used to bypass data flow and directly access the DOM or a component instance.
const Parent = () => {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus();
inputRef.current.value = 'hello';
};
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>操作输入框</button>
</>
);
};
The parent directly manipulates the child’s DOM. This is a “backdoor” in React.
React treats ref specially, for example:
// 这样不行!
const Child = (props) => {
return <input ref={props.myRef} />;
};
const Parent = () => {
const myRef = useRef(null);
return <Child myRef={myRef} />; // ref 传不过去
};
The ref attribute is handled specially by React and is not passed down like ordinary props. That’s why forwardRef is needed.
forwardRef: Let refs pass through components
Its main purpose is to let custom components receive and forward refs. By default, custom components cannot receive refs:
// 报错!
const Parent = () => {
const childRef = useRef(null);
return <ChildComponent ref={childRef} />;
};
Error message: Function components cannot be given refs.
Basic usage
import { forwardRef, useRef } from 'react';
const ChildComponent = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
const Parent = () => {
const childRef = useRef(null);
const handleClick = () => {
childRef.current.focus();
};
return (
<>
<ChildComponent ref={childRef} />
<button onClick={handleClick}>聚焦</button>
</>
);
};
forwardRef is a higher-order component. When React detects a component created by forwardRef, it passes ref as the second argument to the render function:
// 简化实现
function forwardRef(render) {
return {
$$typeof: Symbol.for('react.forward_ref'),
render,
};
}
// 使用时
const Component = forwardRef((props, ref) => {
return <div ref={ref}>...</div>;
});
useImperativeHandle
forwardRef lets refs be passed to child components. But sometimes, you don’t want to expose the entire DOM node, only a few methods.
Exposing the DOM directly is asking for trouble
// 父组件可以做这些事:
childRef.current.focus();
childRef.current.value = 'x';
childRef.current.select();
childRef.current.blur();
Once the parent component gains full control, coupling issues easily arise in the implementation. It can also introduce security concerns, for example, the parent bypassing the child’s business logic to use data directly, or future refactoring of the child breaking the parent.
So you need to customize what the ref exposes, for example:
import { forwardRef, useRef, useImperativeHandle } from 'react';
const CustomInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
getValue: () => inputRef.current.value,
validate: () => inputRef.current.value.length > 0
}), []);
return <input ref={inputRef} {...props} />;
});
The parent component can only call the exposed methods:
const Parent = () => {
const inputRef = useRef(null);
const handleSubmit = () => {
if (inputRef.current.validate()) {
console.log(inputRef.current.getValue());
}
};
return (
<>
<CustomInput ref={inputRef} />
<button onClick={handleSubmit}>提交</button>
</>
);
};
To understand useImperativeHandle, start with the nature of refs. useRef creates a plain object:
function createRef() {
return { current: null };
}
This object stays the same throughout the component’s lifetime. useImperativeHandle replaces ref.current with a custom object.
function useImperativeHandle(ref, createHandle, deps) {
useEffect(() => {
ref.current = createHandle();
return () => { ref.current = null; };
}, deps);
}
The key is that ref.current no longer points to the DOM node, but to the object returned by createHandle(), as shown below
graph TD
A[父组件: useRef] --> B[ref 对象]
B --> C[forwardRef 子组件]
C --> D[内部 useRef]
C --> E[useImperativeHandle]
E --> F[ref.current = 自定义对象]
F --> G[父组件调用方法]
G --> H[实际执行内部方法]
ref is a mutable shared object, and both parent and child components access the same reference:
- The parent component creates a ref:
const parentRef = useRef(null) - and passes it to the child component
- The child component calls
useImperativeHandle(parentRef, () => ({ ... }), []) - which modifies parentRef.current
- When the parent component accesses it, it gets the custom object
Use cases
flowchart TD
A[需要子组件的数据/行为?] --> B{是主动还是被动?}
B -->|被动: 子组件通知| C[用回调]
B -->|主动: 父组件触发| D{回调能解决吗?}
D -->|能| C
D -->|不能| E[用 ref + forwardRef]
Prefer callbacks, and only use ref when callbacks truly can’t solve the problem, because
- callbacks are declarative, making the code’s intent clearer
- data flows in one direction, making debugging easier
- Refactoring the child component has a smaller blast radius
Demo
Editable cells
const EditableCell = forwardRef(({ value, onSave }, ref) => {
const [isEditing, setIsEditing] = useState(false);
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
startEdit: () => {
setIsEditing(true);
setTimeout(() => inputRef.current?.focus(), 0);
},
getValue: () => inputRef.current?.value
}), []);
if (isEditing) {
return (
<input
ref={inputRef}
defaultValue={value}
onBlur={() => onSave(inputRef.current.value)}
/>
);
}
return <span onClick={() => ref.current?.startEdit()}>{value}</span>;
});
Form validation
const ValidatedInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
validate: () => {
const value = inputRef.current.value;
return value.length > 0 && value.includes('@');
},
focus: () => inputRef.current.focus()
}), []);
return <input ref={inputRef} {...props} />;
});
Summary
| Problem | Solution |
|---|---|
| props can’t let the parent call the child component | ref |
| Custom components can’t receive ref | forwardRef |
| Don’t want to expose the entire DOM | useImperativeHandle |
The principle behind useImperativeHandle is simple: replace ref.current with a custom object. It works because ref is a shared mutable object, and parent and child access the same reference.
Callback or ref? Remember three things:
- Child notifies parent proactively → callback
- Parent triggers child proactively → try refactoring with a callback first; fall back to ref if that doesn’t work
- ref is an “escape hatch”, not the first choice