React表单校验错误信息应用至所有表单项问题排查

问题根源
错误提示在所有表单项上展示是状态设计逻辑错误导致的:
- 你将错误标记
hasError、错误提示文案errorMessage定义在页面组件顶层,属于整个表单共用的单一全局状态 - 遍历渲染所有
TextboxScannerElement组件时,你给每一个输入项都传入了同一个全局errorMessage,只要任意一个输入项校验失败触发全局状态更新,所有表单项都会拿到错误文案并展示 - 附带逻辑bug:你把清空错误的逻辑写在了
isCodeExists请求函数中,每次触发校验发请求时都会直接清空全局错误,会覆盖其他表单项已有的校验失败状态。
修复方案
取消全局单一错误状态,将错误状态下沉绑定到每个独立的表单项数据上,每个输入项维护自己的校验状态:
- 移除页面顶层的全局
hasError、errorMessage状态 - 初始化表单数据时,给每个表单项增加独立的
hasError、errorMessage字段 - 输入触发校验时,只更新当前操作项的错误状态,不做全局修改
- 渲染表单项时,只传入当前项自身的错误信息
- 把清空错误的逻辑移到输入触发的时机,不要放在公共请求函数里
- 提交前增加全量校验,存在校验失败项时阻止提交
修复后的完整代码如下:
import React, { useState, useEffect } from 'react'; import TextboxScannerElement from './TextboxScannerElement'; export default function CollectionContainerPage(props) { const [pageUserInput, setPageUserInput] = useState([]); useEffect(() => { if (props.pageData !== undefined) { // 初始化时给每个表单项绑定独立的错误状态字段 const initFormData = props.pageData.map(item => ({ ...item, hasError: false, errorMessage: '' })) setPageUserInput(initFormData); } }, [props.pageData]); async function onUserInput(element, value) { if (typeof element === 'string') { let pageUserInputCopy = [...pageUserInput]; const targetIndex = pageUserInputCopy.findIndex((obj) => obj.key === element); let targetItem = { ...pageUserInputCopy[targetIndex] }; targetItem.userInput = value; // 输入时先清空当前项的历史错误 targetItem.hasError = false; targetItem.errorMessage = ''; pageUserInputCopy[targetIndex] = targetItem; setPageUserInput(pageUserInputCopy); const isInputValid = await isValid(value); if (!isInputValid) { // 仅更新当前校验失败项的错误状态 const updatedList = [...pageUserInputCopy]; const errItemIndex = updatedList.findIndex((obj) => obj.key === element); updatedList[errItemIndex] = { ...updatedList[errItemIndex], hasError: true, errorMessage: "Collection Container ID already exists!" } setPageUserInput(updatedList); } } } function isCodeExists(userInput) { const code = encodeURIComponent(userInput); const testURL = loris.BaseURL + '/biosample/ajax/validateScannableCode.php' + '?scannable_code=' + code; return fetch( testURL, { credentials: 'include', method: 'GET', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, }) .then((response) => response.json()) .then((responseData) => { return responseData === 2 ? 2 : 1; }) .catch(error => console.warn(error)); } async function isValid(userInput) { const checkResult = await isCodeExists(userInput); return checkResult !== 2; } function handleSubmit(e) { // 提交前全量校验,存在错误项阻止提交 const hasInvalidItem = pageUserInput.some(item => item.hasError); if (hasInvalidItem) return; props.updatePageData(pageUserInput); props.nextStep(); } function previous() { props.updatePageData(pageUserInput); props.prevStep(); } const form = pageUserInput.length === 0 ? null : pageUserInput.map(({ label, key, userInput, hasError, errorMessage }) => ( <TextboxScannerElement keyStr={key} label={label} name={key} onUserInput={onUserInput} value={userInput} hasError={hasError} // 仅传入当前项自身的错误信息 errorMessage={errorMessage} /> )); return ( <> <div className="instructions"> <strong>Instructions:</strong> <p>Scan the Collection Containers found in the kit.</p> </div> <FormElement className="FormElement" name="form" fileUpload={false} onSubmit={handleSubmit} onUserInput={onUserInput} > {form} <ButtonElement label="Next" onUserInput={onUserInput} /> </FormElement> <button onClick={previous} type="button" className="btn btn-primary prevButton" > Back </button> </> ); }
内容的提问来源于stack exchange,提问作者LearningUser
相关产品推荐
相关产品推荐

