You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React受控输入框光标自动跳至末尾问题的解决方法

React受控输入框光标总是跳至末尾的解决办法

问题出在哪

你的代码里有两个直接导致光标乱跳的问题:

  1. bindTextField开头的手机号格式化逻辑,会每次组件渲染时就修改表单值,强制输入框重新渲染,光标直接被重置到末尾。
  2. 输入值被格式化修改后,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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 16:14:52