React受控输入框光标自动跳至末尾问题的解决方法
React受控输入框光标总是跳至末尾的解决办法
问题出在哪
你的代码里有两个直接导致光标乱跳的问题:
bindTextField开头的手机号格式化逻辑,会每次组件渲染时就修改表单值,强制输入框重新渲染,光标直接被重置到末尾。- 输入值被格式化修改后,React受控组件默认会把光标定位到文本最后,而且你没在值更新后手动恢复正确的光标位置。
具体修复步骤
1. 删掉渲染时的自动格式化代码
把bindTextField开头那段格式化代码去掉,别让它每次渲染都瞎改值:
bindTextField = (id: FIELD_TYPE, options?: { autoTrim?: boolean, autoLowerCase?: boolean, formatPhoneNumber?: boolean } & TextFieldHooks) => { const { autoTrim, autoLowerCase, formatPhoneNumber, ...hooks } = (options || {}); const value = this.form.$(id).value; // 删掉下面这段,别在渲染时改值 // if (formatPhoneNumber) { // const formattedPhoneNumber = formatPhoneNumberWithDefaultCountryCode(value); // if (formattedPhoneNumber) { // this.form.$(id).onChange(formattedPhoneNumber); // } // } return { // ... 原有的返回内容 }; };
2. 给输入框加ref,在onChange里手动控制光标位置
首先,在使用这个输入框的组件里创建一个ref:
class YourForm extends React.Component { // 创建输入框的ref phoneInputRef = React.createRef<HTMLInputElement>(); render() { return ( <TextField {...this.bindTextField('user_phone', { formatPhoneNumber: true })} inputRef={this.phoneInputRef} /> ); } }
然后修改bindTextField里的onChange方法,先存光标位置,格式化后再恢复:
bindTextField = (id: FIELD_TYPE, options?: { autoTrim?: boolean, autoLowerCase?: boolean, formatPhoneNumber?: boolean } & TextFieldHooks) => { const { autoTrim, autoLowerCase, formatPhoneNumber, ...hooks } = (options || {}); const value = this.form.$(id).value; return { value: this.form.$(id).value, label: this.form.$(id).label, // ... 其他属性 onChange: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { const input = event.target; // 先记下当前光标在哪 const cursorPos = input.selectionStart; const originalVal = input.value; let newVal = originalVal; // 按需做格式化 if (formatPhoneNumber) { newVal = formatPhoneNumberWithDefaultCountryCode(newVal) || newVal; } if (autoLowerCase) { newVal = typeof newVal === 'string' ? newVal.toLowerCase() : newVal; } // 计算格式化后光标应该在的位置 let newCursorPos = cursorPos; if (formatPhoneNumber && newVal !== originalVal) { // 比如格式化时加了括号、短横线,算一下多了几个字符 const addedChars = newVal.length - originalVal.length; newCursorPos += addedChars; // 别让光标超出文本长度 newCursorPos = Math.min(newCursorPos, newVal.length); } // 更新表单值 this.form.$(id).onChange(newVal); hooks?.onChange?.(event); // 等DOM更新完再恢复光标,用requestAnimationFrame确保时机对 requestAnimationFrame(() => { if (input) { input.setSelectionRange(newCursorPos, newCursorPos); } }); }, // ... 其他属性 }; };
3. 保留失焦时的格式化逻辑
onBlur里的格式化可以留着,毕竟失焦后光标在哪无所谓,主要是确保最终值是对的:
onBlur: (event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => { const inputValue = event?.target?.value; let newValue = inputValue; if (autoTrim) { newValue = typeof inputValue === 'string' ? newValue.trim() : newValue; } if (formatPhoneNumber) { const phoneNumber = formatPhoneNumberWithDefaultCountryCode(newValue); newValue = phoneNumber || newValue; } if (autoLowerCase) { newValue = typeof inputValue === 'string' ? newValue.toLowerCase() : newValue; } if (newValue !== value) { this.form.$(id).onChange(newValue); } this.form.$(id).onBlur(); hooks?.onBlur?.(event); this.onSave(); },
要注意的地方
- 绝对别在组件渲染的时候改表单状态,一触发重渲染光标肯定乱。
- 恢复光标必须等DOM更新完再做,用
requestAnimationFrame比setTimeout更靠谱。 - 如果你的手机号格式化规则特别复杂(比如中间加分隔符),得更精准地算光标位置——比如对比原字符串和格式化后的字符串,找到光标对应的新索引,不能只加字符数。
内容的提问来源于stack exchange,提问作者sayinmehmet47
相关产品推荐
相关产品推荐

