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

React中如何通过表单动态为含数组的对象listItem字段赋值

React 表单动态录入数组类型字段实现方案

核心是通过受控组件模式统一管理全表单状态,针对数组类型的listItem字段单独封装增删改逻辑,所有输入操作都通过不可变数据更新方式同步到表单状态,最终提交时直接从状态中取完整结构即可。


实现步骤

  1. 初始化表单状态
    将给出的业务对象结构作为useState的初始值,顶层普通字段和数组字段统一托管:
import { useState } from 'react';

// 单条listItem的初始空结构,新增行时直接复用
const INITIAL_LIST_ITEM = {
  productName: "",
  quantity: null,
  price: null,
  amount: null,
  gstRate: null
}

// 整个表单初始值
const INITIAL_FORM_VALUE = {
  name: "",
  dueDate: "",
  grossAmount: "",
  billNo: "",
  billDate: "",
  listItem: [{ ...INITIAL_LIST_ITEM }], // 默认带1行空条目
  gstAmount: null,
  netAmount: null,
  notes: "",
  status: ""
}
  1. 绑定顶层普通字段
    顶层的非数组字段(比如name、billNo这类)直接通过name属性匹配字段名,统一写onChange逻辑更新状态即可,不用每个字段单独写事件。

  2. 实现listItem数组的三类操作

  • 单行内容更新:通过数组索引定位当前编辑的条目,不可变更新对应字段值
  • 新增条目:往listItem数组末尾追加一条空结构的新条目
  • 删除条目:过滤掉对应索引的条目,保留剩余条目
  1. 绑定表单提交事件
    给原生<form>标签绑定onSubmit事件,阻止默认提交行为后,拿到的state就是填充完成的完整业务对象,可直接调用接口提交。

完整可运行代码示例

function BillForm() {
  const [formData, setFormData] = useState(INITIAL_FORM_VALUE);

  // 顶层普通字段统一更新逻辑
  const handleTopLevelChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }))
  }

  // listItem单行字段更新逻辑
  const handleListItemChange = (index, e) => {
    const { name, value } = e.target;
    setFormData(prev => {
      const newList = [...prev.listItem];
      newList[index] = {
        ...newList[index],
        [name]: value
      }
      // 可选:自动计算单行金额 = 数量 * 单价
      if (name === 'quantity' || name === 'price') {
        const qty = name === 'quantity' ? Number(value) : Number(newList[index].quantity) || 0;
        const price = name === 'price' ? Number(value) : Number(newList[index].price) || 0;
        newList[index].amount = qty * price;
      }
      return { ...prev, listItem: newList }
    })
  }

  // 新增listItem行
  const addListItem = () => {
    setFormData(prev => ({
      ...prev,
      listItem: [...prev.listItem, { ...INITIAL_LIST_ITEM }]
    }))
  }

  // 删除指定行
  const removeListItem = (index) => {
    setFormData(prev => ({
      ...prev,
      listItem: prev.listItem.filter((_, i) => i !== index)
    }))
  }

  // 表单提交
  const handleSubmit = (e) => {
    e.preventDefault();
    // 此处formData就是填充完成的完整对象,可直接提交
    console.log('提交的完整表单数据:', formData);
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: 800, margin: '20px auto' }}>
      {/* 顶层普通字段区域 */}
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 20 }}>
        <div>
          <label>账单名称:</label>
          <input name="name" value={formData.name} onChange={handleTopLevelChange} />
        </div>
        <div>
          <label>账单编号:</label>
          <input name="billNo" value={formData.billNo} onChange={handleTopLevelChange} />
        </div>
        <div>
          <label>账单日期:</label>
          <input type="date" name="billDate" value={formData.billDate} onChange={handleTopLevelChange} />
        </div>
        <div>
          <label>到期日期:</label>
          <input type="date" name="dueDate" value={formData.dueDate} onChange={handleTopLevelChange} />
        </div>
      </div>

      {/* listItem动态录入区域 */}
      <h4>商品明细</h4>
      {formData.listItem.map((item, index) => (
        <div key={index} style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr 1fr 1fr auto', gap: 8, marginBottom: 8 }}>
          <input 
            placeholder="商品名称" 
            name="productName" 
            value={item.productName} 
            onChange={(e) => handleListItemChange(index, e)} 
          />
          <input 
            type="number" 
            placeholder="数量" 
            name="quantity" 
            value={item.quantity ?? ''} 
            onChange={(e) => handleListItemChange(index, e)} 
          />
          <input 
            type="number" 
            placeholder="单价" 
            name="price" 
            value={item.price ?? ''} 
            onChange={(e) => handleListItemChange(index, e)} 
          />
          <input 
            type="number" 
            placeholder="金额" 
            name="amount" 
            value={item.amount ?? ''} 
            readOnly
          />
          <input 
            type="number" 
            placeholder="税率(%)" 
            name="gstRate" 
            value={item.gstRate ?? ''} 
            onChange={(e) => handleListItemChange(index, e)} 
          />
          {/* 至少保留1行时才显示删除按钮 */}
          {formData.listItem.length > 1 && (
            <button type="button" onClick={() => removeListItem(index)}>删除</button>
          )}
        </div>
      ))}
      <button type="button" onClick={addListItem} style={{ margin: '10px 0' }}>+ 新增商品行</button>

      {/* 剩余顶层字段 */}
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12, margin: '20px 0' }}>
        <div>
          <label>总金额:</label>
          <input name="grossAmount" value={formData.grossAmount} onChange={handleTopLevelChange} />
        </div>
        <div>
          <label>税额:</label>
          <input type="number" name="gstAmount" value={formData.gstAmount ?? ''} onChange={handleTopLevelChange} />
        </div>
        <div>
          <label>净金额:</label>
          <input type="number" name="netAmount" value={formData.netAmount ?? ''} onChange={handleTopLevelChange} />
        </div>
      </div>
      <div>
        <label>备注:</label>
        <textarea name="notes" value={formData.notes} onChange={handleTopLevelChange} rows={3} style={{ width: '100%' }} />
      </div>
      <div>
        <label>状态:</label>
        <select name="status" value={formData.status} onChange={handleTopLevelChange}>
          <option value="">请选择状态</option>
          <option value="unpaid">未支付</option>
          <option value="paid">已支付</option>
        </select>
      </div>

      <button type="submit" style={{ marginTop: 20, padding: '8px 20px' }}>提交账单</button>
    </form>
  )
}

export default BillForm;

注意事项

  • 所有状态更新必须遵循不可变原则,不要直接修改state里的数组或对象,否则会导致组件不重渲染、输入值不更新的问题
  • 简单场景用数组索引作为key即可,如果涉及频繁排序、插入行的操作,建议给每个listItem生成唯一id作为key,避免输入错位
  • 示例里加了单行金额自动计算的逻辑,可以根据业务需求扩展总金额、税额的自动汇总逻辑,不用用户手动输入

内容的提问来源于stack exchange,提问作者Ayan Yadav

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 01:03:24