React TS中使用reactstrap的<Input>字段自定义验证时旧值重复出现的问题求助
问题描述
我在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

