Skip to content

useFieldArray ​

A list of fields: order items, phones, participants. Gives stable keys for v-for and row operations. When rows are removed or reordered, touched marks move with them — an error never jumps to a neighbouring row.

Signature ​

ts
function useFieldArray<T>(path: string): FieldArray<T>
function useFieldArray<T>(form: Form, path: string): FieldArray<T>
ReturnsDescription
fields{ key, index, path, value }[] — key for v-for, path for errors and touch
append(value), prepend(value)Add a row to the end / start
insert(index, value)Insert a row
remove(index)Remove a row
move(from, to), swap(a, b)Reorder rows
replace(values)Replace the whole list

Without form the form comes from the context. If there is no array in values, it is created. Direct changes (form.values.items.push(...)) work too — new rows get keys.

Example ​

vue
<script setup lang="ts">
const Order = object({
  items: pipe(array(object({ title: pipe(string(), minLength(2)), qty: pipe(coerce.number(), min(1)) })), minLength(1)),
});

const form = useForm(Order, { initial: { items: [{ title: '', qty: '1' }] } });
const items = useFieldArray<{ title: string; qty: string }>(form, 'items');
</script>

<template>
  <div v-for="field in items.fields.value" :key="field.key">
    <input v-model="field.value.title" :name="`${field.path}.title`" @blur="form.touch(`${field.path}.title`)" />
    <span>{{ form.errors[`${field.path}.title`] }}</span>

    <input v-model="field.value.qty" :name="`${field.path}.qty`" @blur="form.touch(`${field.path}.qty`)" />

    <button type="button" :disabled="field.index === 0" @click="items.move(field.index, field.index - 1)">↑</button>
    <button type="button" @click="items.remove(field.index)">×</button>
  </div>
  <button type="button" @click="items.append({ title: '', qty: '1' })">+</button>
</template>

Why not the index as :key: removing the first row shifts all indexes, and Vue reuses the wrong rows' inputs — focus and typed text "jump".

See also ​