如何在React JS中使用Formik处理多表单更新?
解决Formik+Yup在多折叠面板更新表单中的两个问题
我来帮你搞定这两个困扰你的问题,咱们一步步拆解原因再给出解决方案:
问题根源分析
你现在遇到的两个问题,本质都是因为所有折叠面板的表单共用了同一个Formik实例:
- 点击提交触发所有表单:因为所有表单都绑定了同一个
updateForm.handleSubmit,所以任何一个提交按钮都会触发这个共享实例的onSubmit方法。 - 无法捕获默认值:你的
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>
关键修改点说明
- 独立Formik实例:把
Formik组件放到循环内部,每个产品的表单都有自己的状态,提交时只会触发当前表单的onSubmit。 - 正确初始化值:
initialValues直接使用当前产品的name和category,打开编辑面板时自动填充现有值。 - 绑定Formik状态值:input的
value改为绑定Formik的values.name,让Formik能正确跟踪用户输入的变化。 - 文件输入特殊处理:文件类型的input无法用常规
handleChange,所以用setFieldValue手动更新字段值,适配文件选择逻辑。 - 唯一ID:修改所有面板和表单元素的ID,添加
product.id后缀,避免多个循环项的ID冲突。 - 优化验证反馈:添加
touched判断,只有用户触碰到输入框后才显示错误信息,提升体验。
这样修改后,你的两个问题应该都能解决啦!
内容的提问来源于stack exchange,提问作者Wahidul Alam
相关产品推荐
相关产品推荐

