如何基于Formik实现含嵌套分类的可添加字段表单?
用Formik实现可添加字段的嵌套分类表单最佳实践
嘿,我之前刚好折腾过类似的Formik表单需求——支持动态增删字段、还有那种层级式的嵌套分类选择,给你分享下我踩坑后总结的最佳实践,应该能解决你的问题!
核心思路:先把表单数据结构捋顺
首先得给Formik的initialValues搭好骨架,动态字段和嵌套分类的逻辑全靠这个结构支撑。我当时是这么定义的:
const initialValues = { products: [ { name: '', price: '', // 用数组存用户选的分类层级路径,比如[一级分类ID, 二级分类ID],方便后续追踪选择步骤 categoryPath: [], // 支持新增分类的话,加个临时输入字段 newCategoryName: '' } ] };
用categoryPath数组记录用户通过箭头选择的分类层级,既能渲染步骤式的选择流程,提交时也能直接拿到完整的分类路径。
具体实现步骤
1. 用useFieldArray搞定动态产品行
Formik自带的useFieldArray是处理动态增删字段的神器,不用手动去修改values或者维护表单状态,它会自动帮你打理数组类型的字段。
我当时写的核心代码片段是这样的:
import { Formik, Form, Field, useFieldArray } from 'formik'; const ProductForm = () => { return ( <Formik initialValues={initialValues} onSubmit={(values) => { // 这里处理表单提交逻辑,比如发请求 console.log('提交的表单数据:', values); }} > {() => ( <Form className="product-form"> <ProductRows /> <button type="submit" className="submit-btn">提交表单</button> </Form> )} </Formik> ); }; const ProductRows = () => { const { fields, append, remove } = useFieldArray({ name: 'products' // 对应initialValues里的products数组 }); // 新增一行产品字段 const addNewProductRow = () => { append({ name: '', price: '', categoryPath: [], newCategoryName: '' }); }; return ( <div className="product-rows"> {fields.map((product, index) => ( <div key={product.id} className="product-row"> {/* 名称输入框 */} <div className="form-group"> <label>产品名称</label> <Field name={`products[${index}].name`} type="text" placeholder="请输入名称" /> </div> {/* 价格输入框 */} <div className="form-group"> <label>产品价格</label> <Field name={`products[${index}].price`} type="number" placeholder="请输入价格" /> </div> {/* 嵌套分类选择组件 */} <CategorySelector rowIndex={index} /> {/* 新增分类功能 */} <div className="form-group"> <label>新增自定义分类</label> <Field name={`products[${index}].newCategoryName`} type="text" placeholder="输入新分类名称" /> <AddNewCategoryBtn rowIndex={index} /> </div> {/* 删除当前行 */} <button type="button" className="delete-btn" onClick={() => remove(index)} > 删除该行 </button> </div> ))} <button type="button" className="add-row-btn" onClick={addNewProductRow} > 新增产品行 </button> </div> ); };
2. 实现步骤式嵌套分类选择
针对那种箭头指引的层级选择,我写了一个独立的CategorySelector组件,根据用户当前的选择路径,动态渲染下一级的分类选项,同时用categoryPath记录选择轨迹。
先假设你的分类树数据是这样的:
// 示例分类树,实际可以从接口获取 const categoryTree = [ { id: 1, name: '电子产品', children: [ { id: 11, name: '手机', children: [{ id: 111, name: '智能手机' }, { id: 112, name: '功能机' }] }, { id: 12, name: '电脑', children: [{ id: 121, name: '笔记本' }, { id: 122, name: '台式机' }] } ]}, { id: 2, name: '服装', children: [ { id: 21, name: '男装', children: [] }, { id: 22, name: '女装', children: [] } ]} ];
然后是分类选择组件的代码:
import { useFormikContext } from 'formik'; const CategorySelector = ({ rowIndex }) => { const { values, setFieldValue } = useFormikContext(); const currentPath = values.products[rowIndex].categoryPath; // 根据当前选择路径,获取下一级可选分类 const getNextLevelCategories = () => { let currentLevel = categoryTree; for (const categoryId of currentPath) { currentLevel = currentLevel.find(item => item.id === categoryId)?.children || []; } return currentLevel; }; // 选择某一级分类,更新路径 const handleSelectCategory = (categoryId) => { const newPath = [...currentPath, categoryId]; setFieldValue(`products[${rowIndex}].categoryPath`, newPath); }; // 回退到上一级选择 const handleGoBack = () => { if (currentPath.length > 0) { const newPath = currentPath.slice(0, -1); setFieldValue(`products[${rowIndex}].categoryPath`, newPath); } }; const nextCategories = getNextLevelCategories(); // 辅助函数:根据ID查找分类名称 const getCategoryNameById = (id, tree = categoryTree) => { for (const item of tree) { if (item.id === id) return item.name; if (item.children) { const found = getCategoryNameById(id, item.children); if (found) return found; } } return ''; }; return ( <div className="category-selector"> {/* 显示已选择的路径 */} <div className="selected-path"> {currentPath.map((id, idx) => ( <span key={id}> {getCategoryNameById(id)} {idx < currentPath.length - 1 && ' → '} </span> ))} {currentPath.length > 0 && ( <button type="button" onClick={handleGoBack}>← 返回上一级</button> )} </div> {/* 渲染下一级分类选项 */} <div className="category-options"> {nextCategories.length > 0 ? ( nextCategories.map(category => ( <button key={category.id} type="button" onClick={() => handleSelectCategory(category.id)} > {category.name} </button> )) ) : ( <span className="final-category">已选择最终分类</span> )} </div> </div> ); };
3. 新增自定义分类的实现
如果要支持用户新增分类,可以写一个按钮组件,点击后把用户输入的分类添加到当前层级的分类树里,同时自动选中这个新分类:
import { useFormikContext } from 'formik'; const AddNewCategoryBtn = ({ rowIndex }) => { const { values, setFieldValue } = useFormikContext(); const currentPath = values.products[rowIndex].categoryPath; const newCategoryName = values.products[rowIndex].newCategoryName; const handleAddNewCategory = () => { if (!newCategoryName.trim()) return; // 生成唯一ID(实际项目可以用后端返回的ID) const newCategoryId = Date.now(); // 找到当前层级的父分类节点 let parentLevel = categoryTree; for (const id of currentPath) { parentLevel = parentLevel.find(item => item.id === id)?.children || []; } // 添加新分类到当前层级 parentLevel.push({ id: newCategoryId, name: newCategoryName, children: [] }); // 自动选中这个新分类 setFieldValue(`products[${rowIndex}].categoryPath`, [...currentPath, newCategoryId]); // 清空输入框 setFieldValue(`products[${rowIndex}].newCategoryName`, ''); }; return ( <button type="button" className="add-category-btn" onClick={handleAddNewCategory} disabled={!newCategoryName.trim()} > 添加该分类 </button> ); };
一些关键细节提示
- 表单验证:可以配合Yup来做,比如验证名称必填、价格是有效数字、分类必须选择到最终层级等,直接在Formik的
validationSchema里配置就行。 - 性能优化:如果分类树很大,记得用
useMemo缓存getNextLevelCategories的结果,避免每次渲染都遍历整个树。 - 分类树持久化:如果新增的分类需要长期保存,提交表单时要把新增的分类数据一起传给后端,不要只存在前端内存里。
这样一套下来,就能实现你需要的所有功能:动态增删产品行、步骤式嵌套分类选择、自定义新增分类,而且完全符合Formik的最佳实践,代码也比较清晰好维护。
内容的提问来源于stack exchange,提问作者Alex
相关产品推荐
相关产品推荐

