{{ errors.firstName }}
\n```\n\n---\n\n## 4. Use computed properties for derived values\n\nAvoid storing values you can calculate.\n\nExample:\n```js\nimport { computed } from 'vue'\n\nconst isFormValid = computed(() => {\n return form.firstName && form.email && !errors.firstName && !errors.email\n})\n```\n\nUse computed values for:\n- enabling/disabling submit buttons\n- calculating totals\n- conditionally showing sections\n\n---\n\n## 5. Manage dynamic fields with arrays\n\nFor repeating fields like phone numbers, dependents, tags, etc., use arrays.\n\n### Example\n```vue\n\n\n\nPrompt
How do I build complex forms in Vue.js?
Latest observation
Building complex forms in Vue.js usually comes down to a few core ideas:
- Keep form state centralized
- Break the form into reusable components
- Use validation rules consistently
- Handle dynamic fields carefully
- Separate UI from business logic
Here’s a practical way to approach it.
1. Start with a single reactive form object
For most forms, keep all fields in one reactive object.
Vue 3 example
<script setup>
import { reactive } from 'vue'
const form = reactive({
firstName: '',
lastName: '',
email: '',
address: {
street: '',
city: '',
zip: ''
},
skills: []
})
</script>
This makes it easier to:
- validate the whole form
- submit in one payload
- reset fields
- save drafts
2. Use components for sections of the form
For complex forms, split the UI into smaller components:
PersonalInfoSection.vueAddressSection.vueSkillsSection.vue
Pass state down with props and emit updates up, or use v-model with custom components.
Example: child component with v-model
<!-- BaseInput.vue -->
<script setup>
const props = defineProps({
modelValue: String,
label: String
})
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<label>
{{ label }}
<input
:value="modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</label>
</template>
Usage:
<BaseInput v-model="form.firstName" label="First name" />
3. Handle validation early
For small forms, you can write custom validation. For bigger forms, use a library like:
- VeeValidate — very popular in Vue
- Zod or Yup for schema validation
- Valibot as a lightweight alternative
Example with simple custom validation
const errors = reactive({
firstName: '',
email: ''
})
function validate() {
errors.firstName = form.firstName ? '' : 'First name is required'
errors.email = /.+@.+\..+/.test(form.email) ? '' : 'Invalid email'
return !errors.firstName && !errors.email
}
Then show errors in the template:
<p v-if="errors.firstName">{{ errors.firstName }}</p>
4. Use computed properties for derived values
Avoid storing values you can calculate.
Example:
import { computed } from 'vue'
const isFormValid = computed(() => {
return form.firstName && form.email && !errors.firstName && !errors.email
})
Use computed values for:
- enabling/disabling submit buttons
- calculating totals
- conditionally showing sections
5. Manage dynamic fields with arrays
For repeating fields like phone numbers, dependents, tags, etc., use arrays.
Example
<script setup>
import { reactive } from 'vue'
const form = reactive({
phones: [{ number: '' }]
})
function addPhone() {
form.phones.push({ number: '' })
}
function removePhone(index) {
form.phones.splice(index, 1)
}
</script>
<template>
<div v-for="(phone, index) in form.phones" :key="index">
<input v-model="phone.number" placeholder="Phone number" />
<button @click="removePhone(index)">Remove</button>
</div>
<button @click="addPhone">Add phone</button>
</template>
Tip: if possible, use stable IDs instead of array indexes as keys.
6. Watch for side effects
Use watch when one field affects another.
Example:
import { watch } from 'vue'
watch(() => form.country, (newCountry) => {
if (newCountry === 'US') {
form.state = ''
}
})
This is useful for:
- clearing dependent fields
- fetching options
- auto-formatting values
7. Submit with a clean payload
Before sending data to the server, transform the form into the shape your API expects.
async function submitForm() {
if (!validate()) return
const payload = {
...form,
fullName: `${form.firstName} ${form.lastName}`.trim()
}
await fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
}
8. Consider using a form library for advanced cases
If your form has:
- lots of validation
- conditional fields
- async validation
- nested objects/arrays
- file uploads
- multi-step flows
then a library helps a lot.
Common stack
- Vue 3 + VeeValidate + Zod
- Or Vue 3 + Pinia if multiple components need shared form state
9. For multi-step forms, store step state separately
Example structure:
const step = ref(1)
const form = reactive({
personal: {},
address: {},
payment: {}
})
Validate each step before moving on:
- step 1: personal info
- step 2: address
- step 3: payment
10. Best practices
- Prefer
v-modelfor inputs - Keep form data in one place
- Use reusable input components
- Validate on blur and on submit
- Avoid too much logic in templates
- Use stable keys for dynamic lists
- Normalize data before submission
- Test edge cases: empty values, async loading, invalid input
Recommended pattern for large forms
A solid architecture looks like this:
FormPage.vue— orchestrates everythingformStore.jsor local reactive state — holds datasections/— UI sectionsBaseInput.vue,BaseSelect.vue,BaseCheckbox.vue— reusable inputsvalidation.ts— schema/rulesapi.ts— submission logic
If you want, I can also show you:
- a full Vue 3 complex form example,
- a multi-step form example, or
- how to build one with VeeValidate + Zod.