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

React千分位输入框:退格/删除键失效问题及简洁实现问询

问题解决:千分位输入框的删除功能修复与简化实现

一、修复现有代码的删除问题

原代码中,按下Backspace/Delete碰到分隔符时仅移动光标,未执行实际删除操作。修改onKeyDown函数,在检测到分隔符时直接删除对应字符并更新状态:

import React, { useState, useRef } from 'react';

const NumericInput = () => {
  const [value, setValue] = useState('');
  const inputRef = useRef(null);

  const onKeyDown = (e) => {
    const inputElement = inputRef.current;
    const caretStart = inputElement.selectionStart;
    const caretEnd = inputElement.selectionEnd;

    // 处理Backspace删除分隔符
    if (e.key === 'Backspace' && caretStart === caretEnd && caretStart > 0) {
      const previousChar = inputElement.value.charAt(caretStart - 1);
      if (previousChar === ',') {
        e.preventDefault();
        // 计算原始值中对应的位置(减去前面分隔符的数量)
        const separatorCountBefore = inputElement.value.slice(0, caretStart - 1).match(/,/g)?.length || 0;
        const originalPosition = caretStart - 1 - separatorCountBefore;
        // 更新原始值,删除对应位置的字符
        const newOriginalValue = value.slice(0, originalPosition) + value.slice(originalPosition + 1);
        setValue(newOriginalValue);
        // 延迟设置光标,确保状态更新后输入框已重新渲染
        setTimeout(() => {
          inputElement.setSelectionRange(caretStart - 1, caretStart - 1);
        }, 0);
        return;
      }
    }

    // 处理Delete删除分隔符
    if (e.key === 'Delete' && caretStart === caretEnd && caretEnd < inputElement.value.length) {
      const nextChar = inputElement.value.charAt(caretStart);
      if (nextChar === ',') {
        e.preventDefault();
        // 提取去掉分隔符后的原始数字值
        const newFormattedValue = inputElement.value.slice(0, caretStart) + inputElement.value.slice(caretStart + 1);
        const newOriginalValue = newFormattedValue.replace(/[^0-9-]/g, '');
        setValue(newOriginalValue);
        // 光标保持在当前位置
        setTimeout(() => {
          inputElement.setSelectionRange(caretStart, caretStart);
        }, 0);
        return;
      }
    }

    // 限制输入只能是数字和负号
    if (!/^-?\d*$/g.test(value + e.key) && !['-', 'Backspace', 'Delete'].includes(e.key)) {
      e.preventDefault();
    }
  };

  const onChange = (e) => {
    const inputValue = e.target.value.replace(/[^0-9-]/g, '');
    setValue(inputValue);

    const inputElement = inputRef.current;
    // 计算光标偏移量:新格式化字符串的分隔符数量减去旧的
    const oldSeparatorCount = formatValue(value).match(/,/g)?.length || 0;
    const newSeparatorCount = formatValue(inputValue).match(/,/g)?.length || 0;
    const caretPosition = Math.max(0, inputElement.selectionStart + (newSeparatorCount - oldSeparatorCount));
    inputElement.setSelectionRange(caretPosition, caretPosition);
  };

  const formatValue = (value) => (value ? value.replace(/\B(?=(\d{3})+(?!\d))/g, ',') : '');

  return (
    <input
      ref={inputRef}
      placeholder='Type Number'
      type='text'
      onKeyDown={onKeyDown}
      onChange={onChange}
      value={formatValue(value)}
    />
  );
};

export default NumericInput;

关键修改点:

  • Backspace逻辑:检测到前一个字符是分隔符时,阻止默认行为,计算原始值对应的位置并更新状态,最后调整光标到删除后的位置。
  • Delete逻辑:检测到后一个字符是分隔符时,阻止默认行为,移除输入框中的分隔符并提取新的原始数字值更新状态,光标保持当前位置。
  • 优化输入限制规则,避免非法字符输入。

二、更简洁的实现方式

手动处理光标和格式化容易出错,推荐两种简化方案:

1. 使用第三方库:react-number-format

这是专门用于数字输入格式化的React库,内置千分位、货币等格式化规则,自动处理光标和删除逻辑:

import React from 'react';
import NumberFormat from 'react-number-format';

const NumericInput = () => {
  return (
    <NumberFormat
      placeholder='Type Number'
      thousandSeparator={true}
      allowNegative={true}
      type='text'
    />
  );
};

export default NumericInput;

该库支持自定义分隔符、前缀后缀、精度控制等功能,完全无需手动处理光标和删除逻辑,代码简洁可靠。

2. 封装自定义Hook简化逻辑

如果不想依赖第三方库,可将核心逻辑封装成Hook,提升代码复用性:

import React, { useState, useRef, useCallback } from 'react';

const useNumberFormatter = (initialValue = '') => {
  const [rawValue, setRawValue] = useState(initialValue.replace(/[^0-9-]/g, ''));
  const inputRef = useRef(null);

  const formatValue = useCallback((value) => {
    return value ? value.replace(/\B(?=(\d{3})+(?!\d))/g, ',') : '';
  }, []);

  const handleKeyDown = useCallback((e) => {
    const elem = inputRef.current;
    const start = elem.selectionStart;
    const end = elem.selectionEnd;

    // Backspace删除分隔符
    if (e.key === 'Backspace' && start === end && start > 0) {
      const prevChar = elem.value.charAt(start - 1);
      if (prevChar === ',') {
        e.preventDefault();
        const sepCount = elem.value.slice(0, start - 1).match(/,/g)?.length || 0;
        const rawPos = start - 1 - sepCount;
        setRawValue(prev => prev.slice(0, rawPos) + prev.slice(rawPos + 1));
        setTimeout(() => elem.setSelectionRange(start - 1, start - 1), 0);
      }
    }

    // Delete删除分隔符
    if (e.key === 'Delete' && start === end && end < elem.value.length) {
      const nextChar = elem.value.charAt(start);
      if (nextChar === ',') {
        e.preventDefault();
        const newFormatted = elem.value.slice(0, start) + elem.value.slice(start + 1);
        setRawValue(newFormatted.replace(/[^0-9-]/g, ''));
        setTimeout(() => elem.setSelectionRange(start, start), 0);
      }
    }

    // 输入限制
    if (!/^-?\d*$/g.test(rawValue + e.key) && !['-', 'Backspace', 'Delete'].includes(e.key)) {
      e.preventDefault();
    }
  }, [rawValue]);

  const handleChange = useCallback((e) => {
    const input = e.target.value.replace(/[^0-9-]/g, '');
    setRawValue(input);
    const elem = inputRef.current;
    const oldSepCount = formatValue(rawValue).match(/,/g)?.length || 0;
    const newSepCount = formatValue(input).match(/,/g)?.length || 0;
    const caretPos = Math.max(0, elem.selectionStart + (newSepCount - oldSepCount));
    setTimeout(() => elem.setSelectionRange(caretPos, caretPos), 0);
  }, [rawValue, formatValue]);

  return {
    inputRef,
    formattedValue: formatValue(rawValue),
    handleKeyDown,
    handleChange
  };
};

const NumericInput = () => {
  const { inputRef, formattedValue, handleKeyDown, handleChange } = useNumberFormatter('');

  return (
    <input
      ref={inputRef}
      placeholder='Type Number'
      type='text'
      onKeyDown={handleKeyDown}
      onChange={handleChange}
      value={formattedValue}
    />
  );
};

export default NumericInput;

将格式化、光标处理、输入限制逻辑封装成Hook后,组件代码更简洁,逻辑复用性更强。

内容的提问来源于stack exchange,提问作者Digitalwolf

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 17:37:06