如何避免自定义MUI TextField在重渲染时失去焦点
解决MUI多色文本输入框输入时失焦问题
问题重现
自定义了支持多色文本的MUI TextField组件,但每次输入内容变更时,输入框都会失去焦点,尝试设置autoFocus无效。
核心原因
- 组件实例重复创建:在
InputProps的inputComponent中直接定义forwardRef组件,每次父组件重渲染时都会生成一个全新的组件实例,React会卸载旧组件并挂载新组件,导致焦点丢失。 - 直接修改props:在
MultiColorTextField中直接修改传入的props对象,违反React不可变数据原则,可能触发额外重渲染。
解决方案
1. 提取自定义输入组件到外部
不要在父组件的渲染函数内定义inputComponent对应的组件,确保组件类型稳定:
// 定义在父组件外部,避免每次重渲染生成新组件类型 const CustomInputComponent = forwardRef((props: any, inputRef: any) => { const { subInputRef, setSubData, subData, focussedField } = props; return MultiColorTextField( props, inputRef, subInputRef, setSubData, subData, focussedField ); });
2. 修复MultiColorTextField的props修改问题
不要直接修改传入的props,而是合并新属性:
export default function MultiColorTextField( props: any, inputRef: any, subInputRef: any, setSubData: any, data: any, focussedField: any ) { // 合并属性,避免修改原props const mergedProps = { ...props, dangerouslySetInnerHTML: { __html: data }, }; return ( <Typography ref={inputRef} {...mergedProps} component="div" variant="body1" contentEditable suppressContentEditableWarning onInput={(evt: any) => { // 记录当前光标位置,解决光标跳开头问题 const selection = window.getSelection(); const range = selection?.getRangeAt(0); const cursorOffset = range?.startOffset; setSubData(evt.target.innerText); // 状态更新后恢复光标位置 setTimeout(() => { if (selection && range && inputRef.current) { selection.removeAllRanges(); const newRange = document.createRange(); const targetNode = inputRef.current.firstChild || inputRef.current; newRange.setStart(targetNode, cursorOffset || 0); newRange.collapse(true); selection.addRange(newRange); inputRef.current.focus(); } }, 0); }} onFocus={() => { focussedField.current = props.id; }} ></Typography> ); }
3. 优化父组件的InputProps配置
使用useMemo稳定传递给自定义组件的props,减少不必要重渲染:
const YourParentComponent = () => { const [subData, setSubData] = useState(""); const subInputRef = useRef(null); const focussedField = useRef(""); // 用useMemo稳定props,仅在subData变化时更新 const inputComponentProps = useMemo(() => ({ subInputRef, setSubData, subData, focussedField, }), [subData]); return ( <TextField id="id-alarm-msg-subject" label={t("AlarmMsgTmp.Create.Subject")} variant="standard" InputLabelProps={{ shrink: true }} inputProps={{ contentEditable: true, suppressContentEditableWarning: true, dangerouslySetInnerHTML: { __html: alarmMsgTmp.formatMultiColorString(subData), } as any, }} InputProps={{ inputComponent: (props) => <CustomInputComponent {...props} {...inputComponentProps} />, ref: subInputRef, sx: { "& .MuiInputBase-input": { overflow: "auto", height: "100px", }, }, }} fullWidth onKeyDown={(evt: any) => { if (evt.which === 13) { evt.preventDefault(); } }} ></TextField> ); };
额外说明
- 移除了
autoFocus属性,因为它仅在组件首次挂载时生效,重渲染时无法恢复焦点,我们通过onInput里的手动焦点恢复逻辑替代。 - 光标位置记录与恢复逻辑是可选的,但能解决状态更新后光标跳到输入框开头的问题,提升用户体验。
内容的提问来源于stack exchange,提问作者NIV
相关产品推荐
相关产品推荐

