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

如何在React JS中使用Formik处理多表单更新?

解决Formik+Yup在多折叠面板更新表单中的两个问题

我来帮你搞定这两个困扰你的问题,咱们一步步拆解原因再给出解决方案:

问题根源分析

你现在遇到的两个问题,本质都是因为所有折叠面板的表单共用了同一个Formik实例:

  1. 点击提交触发所有表单:因为所有表单都绑定了同一个updateForm.handleSubmit,所以任何一个提交按钮都会触发这个共享实例的onSubmit方法。
  2. 无法捕获默认值:你的initialValues设为空字符串,同时input直接绑定product.name而不是Formik的values,导致Formik的内部状态和实际显示值脱节,提交时自然拿不到正确的默认值。

解决方案:给每个表单分配独立的Formik实例

我们需要把Formik的实例创建逻辑放到products.map循环内部,让每个产品的编辑表单都有自己独立的状态管理。同时修正表单值的绑定方式,让Formik能正确跟踪状态变化。

修改后的完整代码示例

<div className="col-lg-7"> 
  <div className="card-style mb-30"> 
    <p className="text-sm mb-20"> Total products: {products.length} </p> 
    { products.length > 0 && products.map((product) => ( 
      <div key={product.id} className="accordion mb-2" id={`accordion-${product.id}`}> {/* 确保ID唯一,避免冲突 */}
        <div className="accordion-item"> 
          <div className="accordion-header p-3" id={`heading-${product.id}`}> 
            <img src={require(`../../../../public/bvend/products/${product.image}`)} className="image-in-list" alt={product.name} /> 
            <span className="ms-3">{product.name}</span> 
            <span className="ms-3">{product.category}</span> 
            <span className="float-end"> 
              <button onClick={() => showEditPanel(product.id)} className='p-0 text-primary btn-link btn'> 
                <i className="lni lni-pencil-alt"></i> 
              </button> 
              <button onClick={() => showRemovePanel(product.id)} className='p-0 ms-2 text-danger btn-link btn'> 
                <i className="lni lni-trash-can"></i> 
              </button> 
            </span> 
          </div> 
          <div id={`edit-panel-${product.id}`} className={editPanel === product.id ? 'd-block' : 'd-none'}> 
            <div className="accordion-body bg-light">
              {/* 每个产品的表单用独立的Formik组件包裹 */}
              <Formik
                initialValues={{ 
                  name: product.name, 
                  category: product.category, 
                  image: '' // 文件输入无法绑定初始值,默认留空
                }}
                validationSchema={yup.object({ 
                  name: yup.string().min(2, "Product name should be at least 2 characters").required('Product name is required'), 
                  category: yup.string().required('Product category is required'), 
                  image: yup.string().required('Product image is required'), 
                })}
                onSubmit={(values, { setSubmitting }) => {
                  // 这里可以处理更新逻辑,比如把values和product.id结合
                  console.log(`更新产品ID: ${product.id}`, values);
                  // updateProduct({...product, ...values})
                  setSubmitting(false);
                  // 提交后可以关闭编辑面板
                  showEditPanel(null);
                }}
              >
                {({ handleSubmit, handleChange, handleBlur, values, errors, touched, setFieldValue }) => (
                  <Form noValidate onSubmit={handleSubmit}>
                    <Form.Group controlId={`validationCustom01-${product.id}`} className="input-style-1">
                      <Form.Control 
                        className={errors.name && touched.name ? "is-invalid" : ""} 
                        name="name" 
                        required 
                        type="text" 
                        placeholder="Enter product name" 
                        onChange={handleChange} 
                        onBlur={handleBlur} 
                        value={values.name} // 绑定Formik的内部状态值,而非直接绑定product.name
                      />
                      {errors.name && touched.name && (
                        <Form.Control.Feedback type="invalid">
                          {errors.name}
                        </Form.Control.Feedback>
                      )}
                    </Form.Group>
                    <Form.Group controlId={`validationCustom02-${product.id}`} className="select-style-2">
                      <CategorySelect 
                        formik={{ handleChange, handleBlur, values, errors, touched }} 
                        filterOption={product.category} 
                      />
                      {errors.category && touched.category && (
                        <Form.Control.Feedback type="invalid">
                          {errors.category}
                        </Form.Control.Feedback>
                      )}
                    </Form.Group>
                    <Form.Group controlId={`formFileLg-${product.id}`} className="input-style-1">
                      <Form.Control 
                        className={errors.image && touched.image ? "is-invalid" : ""} 
                        name="image" 
                        required 
                        type="file" 
                        placeholder="Choose product image" 
                        onChange={(e) => {
                          // 文件输入需要手动处理,因为无法用常规handleChange绑定
                          const file = e.target.files[0];
                          if (file) {
                            // 这里可以选择保存文件名(如示例)或文件对象(如果需要上传)
                            setFieldValue('image', file.name);
                            // 如果需要上传文件,改用:setFieldValue('image', file);
                          }
                        }}
                        onBlur={handleBlur} 
                      />
                      {errors.image && touched.image && (
                        <Form.Control.Feedback type="invalid">
                          {errors.image}
                        </Form.Control.Feedback>
                      )}
                    </Form.Group>
                    <div className="d-grid gap-2 d-md-flex justify-content-md-end">
                      <Button type="submit" className="main-btn primary-btn btn-hover btn-sm">Update Product</Button>
                    </div>
                  </Form>
                )}
              </Formik>
            </div> 
          </div> 
          <div id={`remove-panel-${product.id}`} className={removePanel === product.id ? 'd-block' : 'd-none'}> 
            <div className="accordion-body bg-light"> 
              <h3>Are you sure to remove?</h3> 
              <div className="d-grid gap-2 d-md-flex justify-content-md-end mt-4"> 
                <button onClick={() => removeProduct(product.id)} className="main-btn danger-btn btn-hover btn-sm">Move to trash</button> 
                <button onClick={() => setRemovePanel(null)} className="main-btn light-btn btn-hover btn-sm">Cancel</button> 
              </div> 
            </div> 
          </div> 
        </div> 
      </div> 
    )) } 
  </div> 
</div>

关键修改点说明

  1. 独立Formik实例:把Formik组件放到循环内部,每个产品的表单都有自己的状态,提交时只会触发当前表单的onSubmit。
  2. 正确初始化值:initialValues直接使用当前产品的name和category,打开编辑面板时自动填充现有值。
  3. 绑定Formik状态值:input的value改为绑定Formik的values.name,让Formik能正确跟踪用户输入的变化。
  4. 文件输入特殊处理:文件类型的input无法用常规handleChange,所以用setFieldValue手动更新字段值,适配文件选择逻辑。
  5. 唯一ID:修改所有面板和表单元素的ID,添加product.id后缀,避免多个循环项的ID冲突。
  6. 优化验证反馈:添加touched判断,只有用户触碰到输入框后才显示错误信息,提升体验。

这样修改后,你的两个问题应该都能解决啦!

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.27 13:58:16