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

React TS中使用reactstrap的<Input>字段自定义验证时旧值重复出现的问题求助

React TS中使用reactstrap的字段自定义验证时旧值重复出现的问题求助

问题描述

我在React TypeScript项目里遇到了一个奇怪的问题:当我在表单页面填写值并保存后,返回该页面时,输入框能正确显示请求存储中的旧值,但当我清空输入框(触发验证)并点击“下一步”按钮时,输入框会自动恢复为旧值,而不是保持清空状态并显示验证错误。

举个具体例子:

  • 输入框初始值:test123
  • 我把值改成空字符串""
  • 点击“下一步”触发验证后,输入框又变回了test123,而不是保持空并提示必填错误

下面是我的相关代码,麻烦帮我看看问题出在哪?

1. 组件初始状态与表单设置

const initialState = {
  name: '',
};

const [registeredValidations, setRegisteredValidations] = useState({} as RegisteredValidationMap);
const [formData, setFormData] = useState({
  ...initialState,
  name: requestStore.formData.name[0] || initialState.name
});

2. 表单元素与导航组件

<ValidatedInput
  name="name"
  value={formData.name}
  onChange={handleOnChange}
  validations={[GenericInputValidation]}
  isOptional={!fieldsRequired}
  setRegisteredValidations={setRegisteredValidations}
  registeredValidations={registeredValidations}
/>
<Row>
  <Navigation handleOnNextClick={handleOnNextClick} registeredValidations={registeredValidations} />
</Row>

3. onChange事件处理

const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => {
  const { name, value } = e.target;
  
  setFormData(prevState => ({
    ...prevState,
    [name]: value,
  }));
};

4. 通用输入验证类

export class GenericInputValidation extends Validation {
  // eslint-disable-next-line @typescript-eslint/require-await
  static async validate(props: ValidationProps): Promise<string[]> {
    const errors = [];

    if (props.newValue === '' && !props.isOptional) {
      errors.push(translate('error.missing-value'));
    }

    return errors;
  }
}
export default GenericInputValidation;

5. 自定义ValidatedInput组件

export const ValidatedInput = (props: ValidatedInputProps) => {
  const [errors, setErrors] = useState([...(props.externalErrors || [])]);

  const errorMessages = filterMessagesByType(errors, ERROR_TEMPLATES) || [];
  const warningMessages = filterMessagesByType(errors, WARNING_TEMPLATES) || [];
  
  let css = '';
  if(errorMessages.length !== 0){
    css= 'validation-failed'
  }else if(warningMessages.length !== 0){
    css = 'validation-warning'
  }

  const DOMprops = getDomProps(props);
  registerValidationComponent(props, errorMessages.length > 0 ? errorMessages : warningMessages, setErrors);

  return (
    <div>
      <Input
        {...DOMprops}
        onBlur={e => {
          generateBlur<HTMLInputElement>(props, errors, setErrors)(e);
        }}
        id={props.id || props.name}
        className={css}
        key={props.id + errors.length}
        readOnly={props.readOnly}
        type={props.type}
        maxLength={props.maxLength}
      />
      {errors.length !== 0 && (
        <Tooltip anchorSelect={`#${props.id || props.name}`} place="top">
          {getErrorsForTooltip(errors)}
        </Tooltip>
      )}
    </div>
  );
};

export default ValidatedInput;

6. 导航组件(Next按钮逻辑)

const navigate = useNavigate();
const dispatch = useDispatch();
const defaultBack = () => {
  navigate(-1);
};
const [warningAcknowledged, setWarningAcknowledged] = useState(false);
const handleBackClick = () => {
  dispatch(setWarningsAcknowledged(false));
  setWarningAcknowledged(false);
  if (props.handleOnBackClick) {
    props.handleOnBackClick();
  } else {
    defaultBack();
  }
};

useEffect(() => {
  setWarningAcknowledged(false);
}, [JSON.stringify(props.registeredValidations)]);

const nextFunction = async () => {
  const foundErrors = [];
  for (const registered of Object.values(props.registeredValidations)) {
    const { value, validations, customValidations, errors, setErrors } = registered;
    const validationProps: ValidationProps = {
      name: registered.name,
      newValue: value,
      validations,
      customValidations,
      errors,
      setErrors,
      isOptional: registered.isOptional,
      maxLength: registered.maxLength,
    };

    foundErrors.push(...(await runValidations(validationProps)));
  }

  const errorMessages = filterMessagesByType(foundErrors, ERROR_TEMPLATES);
  const warningMessagesFiltered = filterMessagesByType(foundErrors, WARNING_TEMPLATES);
  const infoMessages = filterMessagesByType(foundErrors, INFO_TEMPLATES);
  const hintMessages = filterMessagesByType(foundErrors, HINT_TEMPLATES);
  const warningMessagesAndInfo = warningMessagesFiltered.concat(infoMessages);
  const warningMessages = warningMessagesAndInfo.concat(hintMessages);

  if (errorMessages.length > 0) {
    return;
  } else if (warningMessages.length > 0 && !warningAcknowledged) {
    setWarningAcknowledged(true);
    return;
  }

  props.handleOnNextClick();
}

return (
  <Container className="mt-4">
    <Row>
      <Col xs={6}>
        {!props.hideBack && (
          <Button name={'zurück' + (props.nameSuffix || '')} id={'back-button' + (props.nameSuffix || '')} onClick={handleBackClick}>
            <FontAwesomeIcon icon={faArrowLeft}></FontAwesomeIcon> <Translate contentKey="generic.back" />
          </Button>
        )}
      </Col>

      <Col xs={6} className="next-button-col">
        {!props.hideNext && (
          <Button
            name={'weiter' + (props.nameSuffix || '')}
            id={'next-button' + (props.nameSuffix || '')}
            onClick={() => {
              nextFunction();
            }}
          >
            <Translate contentKey="generic.next" /> <FontAwesomeIcon icon={faArrowRight}></FontAwesomeIcon>
          </Button>
        )}
      </Col>
    </Row>
  </Container>
);

问题分析与解决方案

嘿,我仔细看了你的代码和问题描述,发现核心问题应该是验证逻辑里使用的value不是最新的表单值,或者ValidatedInput的key设置导致组件不必要重挂载,进而恢复旧值。下面是具体的排查点和修复方案:

1. 先修复ValidatedInput的key设置问题

你给Input组件加了key={props.id + errors.length},当验证错误状态变化时,这个key会改变,导致Input组件被重新卸载再挂载。重新挂载时如果意外读取了requestStore的旧值,就会恢复成原来的内容。

修复方案:
把key改成只和输入框的唯一标识(比如name)绑定,避免不必要的组件重渲染:

<Input
  {...DOMprops}
  onBlur={e => {
    generateBlur<HTMLInputElement>(props, errors, setErrors)(e);
  }}
  id={props.id || props.name}
  className={css}
  key={props.name} // 这里修改
  readOnly={props.readOnly}
  type={props.type}
  maxLength={props.maxLength}
/>

2. 验证时使用最新的表单值,而非注册时的旧值

在nextFunction里,你从registeredValidations中取value做验证,但registeredValidations里存储的可能是组件注册时的初始值,不是实时的表单值。

修复方案:
直接从formData中取最新值来做验证:

const nextFunction = async () => {
  const foundErrors = [];
  // 遍历formData的字段,确保取到最新值
  for (const fieldName of Object.keys(formData)) {
    const registered = props.registeredValidations[fieldName];
    if (!registered) continue;
    
    // 从formData获取实时值
    const currentValue = formData[fieldName];
    const validationProps: ValidationProps = {
      name: fieldName,
      newValue: currentValue, // 这里修改
      validations: registered.validations,
      customValidations: registered.customValidations,
      errors: registered.errors,
      setErrors: registered.setErrors,
      isOptional: registered.isOptional,
      maxLength: registered.maxLength,
    };

    foundErrors.push(...(await runValidations(validationProps)));
  }

  // 后续逻辑保持不变...
}

3. 确保registeredValidations实时同步最新值

你的ValidatedInput组件里调用了registerValidationComponent,如果这个函数没有在value变化时更新registeredValidations,就会导致存储的是旧值。

修复方案:
在ValidatedInput里加一个useEffect,监听props.value的变化,同步更新到registeredValidations:

// 放在ValidatedInput组件的开头
useEffect(() => {
  if (props.name) {
    props.setRegisteredValidations(prev => ({
      ...prev,
      [props.name]: {
        ...prev[props.name],
        value: props.value
      }
    }));
  }
}, [props.value, props.name, props.setRegisteredValidations]);

4. 检查handleOnNextClick是否意外修改了全局状态

最后,确认你的handleOnNextClick函数,不要在验证失败时把requestStore里的旧值同步回formData。只有当验证完全通过时,才把表单值保存到全局状态里。

总结

最可能的原因是Input组件的key导致不必要的重挂载,或者registeredValidations里存储的不是实时表单值。先从修改key开始排查,这个是最容易复现和修复的点,然后再逐步验证其他逻辑。

备注:内容来源于stack exchange,提问作者Dilyan Galabov

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.14 10:39:52