
本文介绍如何在 react(尤其是 next.js)中处理需按数量重复渲染、且每个重复项需独立收集用户输入的嵌套数据结构,重点解决字段唯一性、状态映射与可维护表单管理问题。
在构建数据驱动的表单应用(如订单配置、问卷嵌套包、多实例问答等)时,常遇到一类典型场景:后端返回的数据包含「按数量展开」的嵌套数组(例如 packages 中每个对象带 quantity 字段),而每个展开后的实例还需绑定一组独立的用户输入字段(如 questions)。直接用 map 展平渲染虽能展示结构,但若缺乏精准的状态建模,极易导致输入框值互相覆盖、提交数据错位或难以校验。
✅ 正确建模:从“展平渲染”到“语义化状态树”
核心问题不在渲染逻辑,而在状态设计是否与业务语义对齐。原方案中通过 useEffect 手动构造扁平数组 temporaryPackageData,虽实现了视觉重复,却丢失了原始数据的层级关系和上下文标识(如属于哪个 package、对应哪条 question),致使后续为每个 分配唯一 name 或 key 时无据可依。
推荐采用结构即状态(Structure-as-State) 方式重构:
// 定义清晰的 TypeScript 类型,反映真实业务含义 interface QuestionItem { id: string; // 唯一标识,避免依赖 index question: string; answer: string; } interface PackageItem { id: string; name: string; quantity: number; questions: QuestionItem[]; // 每个 package 实例预置其专属 question 列表 } // 初始化:将原始响应转换为带完整状态树的结构 const initializePackages = (response: ApiResponse): PackageItem[] => { return response.packages.map(pkg => ({ id: `pkg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, // 生产环境建议用 UUID name: pkg.packageA || 'Unknown Package', quantity: Number(pkg.quantity) || 1, questions: Array.from({ length: Number(pkg.quantity) }, (_, i) => ({ id: `q-${pkg.packageA}-${i}`, question: response.questions[i % response.questions.length]?.question1 || `Question ${i + 1}`, answer: '' })) })); };
? 关键点:questions 数组长度严格等于 quantity,每个 QuestionItem 拥有稳定 id(非数组索引),确保 react key 稳定、表单字段可精准绑定。
? 渲染与受控输入:使用 Formik FieldArray(推荐)或自定义 Hook
借助 Formik 的 FieldArray 可极大简化动态列表管理。它自动处理 push/remove/insert 的状态更新,并支持深层嵌套路径(如 packages[0].questions[2].answer):
import { Form, Field, FieldArray, useFormikContext } from 'formik'; // 在 Form 组件内使用