需求背景
在 React 应用开发中,表单控件通常有两种使用模式:受控模式
和非受控模式
。这两种模式满足了不同的开发场景需求:
-
受控模式:表单值完全由父组件通过 props 控制,组件本身不维护内部状态
- 适用于:复杂表单逻辑、数据验证、条件显示、表单值联动等场景
- 特点:更灵活,可以实时获取/操作输入值
-
非受控模式:表单值由组件内部状态管理
- 适用于:简单表单、独立的输入场景
- 特点:代码简洁、独立
如何让组件既支持受控模式
又支持非受控模式
?本文就跟大家分享一下我是怎么解决这个问题的,包括我的思路和一些实现细节,欢迎各位大佬留言指正~
技术难点
实现同时支持两种模式的 Input 组件面临以下技术难点:
-
状态管理的双重逻辑:
- 如何判断当前是受控还是非受控模式
- 如何在两种模式间无缝切换
-
值同步问题:
- 受控模式下,组件内部状态需要与外部提供的
value
保持同步 - 避免受控/非受控模式混用导致的状态不一致
- 受控模式下,组件内部状态需要与外部提供的
-
事件处理的一致性:
- 确保在不同模式下组件的事件处理表现一致
- 对于特殊操作(如清除按钮)如何保持行为一致性
-
引用透传(Ref Forwarding):
- 如何在两种模式下正确透传引用,使外部能访问到实际的 DOM 节点
实现思路
1. 状态管理与模式识别
typescript
const [inputValue, setInputValue] = useState(value || defaultValue || "");
// 受控模式下同步外部value变化
useEffect(() => {
if (value !== undefined) {
setInputValue(value);
}
}, [value]);
思路:
- 通过
value !== undefined
来判断是否为受控模式 - 使用
useEffect
监听外部value
变化,保持内部状态同步 - 初始状态使用
value || defaultValue || ""
的优先级确保两种模式下行为一致
2. 事件处理与状态更新
typescript
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
if (value === undefined) {
setInputValue(e.target.value);
}
onChange?.(e);
};
思路:
- 在处理变更事件时,先判断是否处于非受控模式 (
value === undefined
) - 非受控模式下,直接更新内部状态
- 无论哪种模式,都调用外部
onChange
回调,保持行为一致
3. 特殊操作处理 - 清除功能
typescript
const handleClear = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
// 创建一个模拟的输入变更事件
const emptyEvent = {
...e,
target: { ...e.target, value: "" },
currentTarget: { ...e.currentTarget, value: "" },
} as unknown as ChangeEvent<HTMLInputElement>;
// 非受控模式下更新状态
if (value === undefined) {
setInputValue("");
}
// 调用外部回调函数
onChange?.(emptyEvent);
onClear?.();
// 恢复输入框焦点
inputRef.current?.focus();
};
思路:
- 将鼠标点击事件转换为标准输入变更事件,保持事件处理一致性
- 在非受控模式下更新内部状态
- 调用专门的
onClear
回调,方便业务层处理清除逻辑 - 模拟正常的用户体验(清除后自动聚焦)
4. 事件处理的一致性保证
为了确保无论是受控还是非受控模式,组件的行为都保持一致,所有事件处理函数都遵循以下模式:
typescript
const handleXXX = (e) => {
// 1. 非受控模式下更新内部状态
if (value === undefined) {
// 更新逻辑
}
// 2. 调用外部回调
props.onXXX?.(e);
// 3. 其他附加逻辑
};
5. Ref 透传实现
typescript
const inputRef = useRef<HTMLInputElement>(null);
// 公开 ref 方法
useImperativeHandle(
ref,
() => ({
focus: () => inputRef.current?.focus(),
blur: () => inputRef.current?.blur(),
input: inputRef.current,
}),
[]
);
思路:
- 创建内部引用保存实际的 DOM 节点
- 使用
useImperativeHandle
暴露方法