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

如何基于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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 07:06:37