TanStack Form supports arrays as values in a form, including sub-object values inside of an array.
To use an array, you can use field.state.value on an array value:
function App() { const form = useForm({ defaultValues: { people: [], }, }) return ( <form.Field name="people" mode="array"> {(field) => ( <div> {field.state.value.map((_, i) => { // ... })} </div> )} </form.Field> ) } function App() { const form = useForm({ defaultValues: { people: [], }, }) return ( <form.Field name="people" mode="array"> {(field) => ( <div> {field.state.value.map((_, i) => { // ... })} </div> )} </form.Field> ) } This will generate the mapped JSX every time you run pushValue on field:
<button onClick={() => field.pushValue({ name: '', age: 0 })} type="button"> Add person </button> <button onClick={() => field.pushValue({ name: '', age: 0 })} type="button"> Add person </button> Finally, you can use a subfield like so:
<form.Field key={i} name={`people[${i}].name`}> {(subField) => ( <input value={subField.state.value} onChange={(e) => subField.handleChange(e.target.value)} /> )} </form.Field> <form.Field key={i} name={`people[${i}].name`}> {(subField) => ( <input value={subField.state.value} onChange={(e) => subField.handleChange(e.target.value)} /> )} </form.Field> function App() { const form = useForm({ defaultValues: { people: [], }, onSubmit({ value }) { alert(JSON.stringify(value)) }, }) return ( <div> <form onSubmit={(e) => { e.preventDefault() e.stopPropagation() form.handleSubmit() }} > <form.Field name="people" mode="array"> {(field) => { return ( <div> {field.state.value.map((_, i) => { return ( <form.Field key={i} name={`people[${i}].name`}> {(subField) => { return ( <div> <label> <div>Name for person {i}</div> <input value={subField.state.value} onChange={(e) => subField.handleChange(e.target.value) } /> </label> </div> ) }} </form.Field> ) })} <button onClick={() => field.pushValue({ name: '', age: 0 })} type="button" > Add person </button> </div> ) }} </form.Field> <form.Subscribe selector={(state) => [state.canSubmit, state.isSubmitting]} children={([canSubmit, isSubmitting]) => ( <button type="submit" disabled={!canSubmit}> {isSubmitting ? '...' : 'Submit'} </button> )} /> </form> </div> ) } function App() { const form = useForm({ defaultValues: { people: [], }, onSubmit({ value }) { alert(JSON.stringify(value)) }, }) return ( <div> <form onSubmit={(e) => { e.preventDefault() e.stopPropagation() form.handleSubmit() }} > <form.Field name="people" mode="array"> {(field) => { return ( <div> {field.state.value.map((_, i) => { return ( <form.Field key={i} name={`people[${i}].name`}> {(subField) => { return ( <div> <label> <div>Name for person {i}</div> <input value={subField.state.value} onChange={(e) => subField.handleChange(e.target.value) } /> </label> </div> ) }} </form.Field> ) })} <button onClick={() => field.pushValue({ name: '', age: 0 })} type="button" > Add person </button> </div> ) }} </form.Field> <form.Subscribe selector={(state) => [state.canSubmit, state.isSubmitting]} children={([canSubmit, isSubmitting]) => ( <button type="submit" disabled={!canSubmit}> {isSubmitting ? '...' : 'Submit'} </button> )} /> </form> </div> ) } 