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

React动态表单渲染及提交/失焦校验实现求助

React动态表单完整实现方案

你已经完成了基础的表单配置渲染和输入值收集,剩下的校验相关功能可以按照下面的步骤补全,代码完全兼容现有配置格式,后续配置变更不需要修改核心逻辑。

核心实现思路

  • 单独维护表单错误状态,和输入值状态分开管理
  • 抽离通用单字段校验函数,提交校验、失焦校验都复用这一个函数,避免重复代码
  • 校验规则完全读取配置里的validation字段,支持扩展新规则无需改动渲染逻辑
  • 给输入框绑定onBlur事件,失焦时触发对应字段的校验

完整实现代码

import { useState } from 'react';

const DYNAMIC_FORMS = [
  {
    texts: {
      header: 'Name Section'
    },
    inputs: [
      {
        label: 'Full Name',
        type: 'text',
        name: 'name',
        validation: {
          required: true,
          minLength: 4
        },
        error: 'Name is required'
      }
    ]
  },
  {
    texts: {
      header: 'Password Section'
    },
    inputs: [
      {
        label: 'Password',
        type: 'password',
        name: 'password',
        validation: {
          required: true,
          minLength: 6,
          maxLength: 10
        },
        error: 'Password is required'
      },
      {
        label: 'Pincode',
        type: 'number',
        name: 'pincode',
        validation: {
          required: true,
          minLength: 6,
          maxLength: 6
        },
        error: 'Invalid Pincode'
      }
    ]
  },
  {
    texts: {
      header: 'Address Section'
    },
    inputs: [
      {
        label: 'Address Line 1',
        type: 'text',
        name: 'addressLine1',
        validation: {
          required: true
        },
        error: 'Invalid Address'
      },
      {
        label: 'Address Line 2',
        type: 'text',
        name: 'addressLine2',
        validation: {
          required: false
        }
      },
      {
        label: 'Zipcode',
        type: 'number',
        name: 'zipcode',
        validation: {
          required: true,
          minLength: 6,
          maxLength: 7
        },
        error: 'Invalid Zipcode'
      }
    ]
  },
  {
    texts: {
      header: 'Thankyou for filling out the form'
    }
  }
];

export default function App() {
  // 存储表单输入值
  const [formValues, setFormValues] = useState({});
  // 存储表单校验错误,key为字段name,value为错误文案
  const [formErrors, setFormErrors] = useState({});

  /**
   * 通用单字段校验函数
   * @param {object} fieldConfig 字段配置项
   * @param {string|number} value 字段当前值
   * @returns {string|null} 有错误返回错误文案,无错误返回null
   */
  const validateField = (fieldConfig, value) => {
    const { validation, error } = fieldConfig;
    // 必填校验
    if (validation.required) {
      if (!value || String(value).trim() === '') {
        return error;
      }
    }
    // 最小长度校验
    if (validation.minLength) {
      if (String(value).length < validation.minLength) {
        return error;
      }
    }
    // 最大长度校验
    if (validation.maxLength) {
      if (String(value).length > validation.maxLength) {
        return error;
      }
    }
    // 后续扩展正则、自定义校验规则,直接在这里加判断即可
    return null;
  };

  // 输入框值变化回调
  const onInputChange = ({ target: { name, value } }) => {
    setFormValues((prev) => ({ ...prev, [name]: value }));
    // 输入时如果该字段已有错误,实时清除,优化体验
    if (formErrors[name]) {
      setFormErrors(prev => {
        const newErrors = {...prev};
        delete newErrors[name];
        return newErrors;
      })
    }
  };

  // 输入框失焦回调,触发单字段校验
  const onInputBlur = (fieldConfig) => {
    const fieldValue = formValues[fieldConfig.name] || '';
    const errorMsg = validateField(fieldConfig, fieldValue);
    setFormErrors(prev => ({
      ...prev,
      [fieldConfig.name]: errorMsg
    }))
  };

  // 表单提交回调
  const onSubmit = (e) => {
    e.preventDefault();
    const allErrors = {};
    // 遍历所有配置字段做全量校验
    DYNAMIC_FORMS.forEach(section => {
      if (!section.inputs) return;
      section.inputs.forEach(field => {
        const fieldValue = formValues[field.name] || '';
        const errorMsg = validateField(field, fieldValue);
        if (errorMsg) {
          allErrors[field.name] = errorMsg;
        }
      })
    })
    setFormErrors(allErrors);
    // 无错误时执行提交逻辑
    if (Object.keys(allErrors).length === 0) {
      console.log('表单校验通过,提交数据:', formValues);
      alert('提交成功!');
    }
  };

  return (
    <div style={{ maxWidth: 500, margin: '40px auto' }}>
      <form autoComplete="off" onSubmit={onSubmit}>
        {DYNAMIC_FORMS.map((section) => (
          <div key={section.texts.header} style={{ marginBottom: 24 }}>
            <h3>{section.texts.header}</h3>
            {section.inputs && (
              <>
                {section.inputs.map((field) => {
                  const { label, name, type } = field;
                  const fieldError = formErrors[name];

                  return (
                    <div key={name} style={{ marginBottom: 16 }}>
                      <label style={{ display: 'block', marginBottom: 4 }}>
                        {label}
                      </label>
                      <input
                        type={type}
                        name={name}
                        value={formValues[name] || ''}
                        onChange={onInputChange}
                        onBlur={() => onInputBlur(field)}
                        style={{
                          width: '100%',
                          padding: '8px 12px',
                          border: fieldError ? '1px solid #f5222d' : '1px solid #d9d9d9',
                          borderRadius: 4,
                          outline: 'none'
                        }}
                      />
                      {/* 错误提示渲染 */}
                      {fieldError && (
                        <div style={{ color: '#f5222d', fontSize: 12, marginTop: 4 }}>
                          {fieldError}
                        </div>
                      )}
                    </div>
                  );
                })}
              </>
            )}
          </div>
        ))}
        <button
          type="submit"
          style={{
            padding: '8px 24px',
            background: '#1677ff',
            color: '#fff',
            border: 'none',
            borderRadius: 4,
            cursor: 'pointer'
          }}
        >
          Submit
        </button>
      </form>
    </div>
  );
}

关键逻辑说明

  • 移除了直接展开validation到input标签的写法:原代码里的{...validation}会把required、minLength这类原生属性绑定到输入框,触发浏览器默认校验弹窗,和自定义JS校验逻辑冲突
  • 通用校验函数做了逻辑复用:提交时的全量校验、失焦时的单字段校验都调用同一个validateField方法,后续调整校验规则只需要修改这一处代码
  • 错误状态独立存储:用单独的formErrors状态存每个字段的错误信息,渲染时按字段名匹配对应提示即可
  • 输入框改为完全受控组件:给input绑定了对应state的value,避免输入值和状态不同步的问题
  • 额外优化了交互体验:用户输入内容时如果该字段已有错误提示,会实时清除,不需要等再次失焦才更新提示
  • 扩展成本极低:后续如果要新增正则校验、自定义校验规则,只需要在validateField里加对应判断,同时在表单配置的validation字段加规则参数即可,不需要调整渲染层代码

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.31 12:03:23