Prompt
How do I create reusable forms in Vue.js?
Latest observation
Aug 27, 2026
OpenAI APIWeb search: off
To create reusable forms in Vue.js, the usual approach is to build a form component that accepts data and configuration via props, emits updates/events to the parent, and keeps the form logic generic.
Common patterns
1) Use a reusable <Form> component
Create a component that handles:
- form rendering
- validation
- submit handling
- error display
Then pass in:
modelValueorformData- a field definition array
- submit/cancel callbacks
2) Use v-model for two-way binding
In Vue 3, use modelValue + update:modelValue so the parent owns the form state.
3) Make fields configurable
Instead of hardcoding inputs, define fields like:
const fields = [
{ name: 'firstName', label: 'First Name', type: 'text' },
{ name: 'email', label: 'Email', type: 'email' },
]
Then render them with v-for.
4) Use slots for customization
Slots let you reuse the same form shell but customize parts like buttons or extra fields.
Example: reusable form component
ReusableForm.vue
<template>
<form @submit.prevent="handleSubmit">
<div v-for="field in fields" :key="field.name" class="mb-4">
<label :for="field.name">{{ field.label }}</label>
<input
:id="field.name"
:type="field.type || 'text'"
v-model="localData[field.name]"
/>
<p v-if="errors[field.name]" class="error">
{{ errors[field.name] }}
</p>
</div>
<slot name="actions">
<button type="submit">Submit</button>
</slot>
</form>
</template>
<script setup>
import { reactive, watch } from 'vue'
const props = defineProps({
modelValue: {
type: Object,
required: true
},
fields: {
type: Array,
required: true
},
errors: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['update:modelValue', 'submit'])
const localData = reactive({ ...props.modelValue })
watch(
() => props.modelValue,
(newVal) => Object.assign(localData, newVal),
{ deep: true }
)
watch(
localData,
(newVal) => emit('update:modelValue', { ...newVal }),
{ deep: true }
)
function handleSubmit() {
emit('submit', { ...localData })
}
</script>
Parent usage
<template>
<ReusableForm
v-model="user"
:fields="fields"
:errors="errors"
@submit="saveUser"
/>
</template>
<script setup>
import { ref } from 'vue'
import ReusableForm from './ReusableForm.vue'
const user = ref({
firstName: '',
email: ''
})
const fields = [
{ name: 'firstName', label: 'First Name', type: 'text' },
{ name: 'email', label: 'Email', type: 'email' }
]
const errors = ref({})
function saveUser(data) {
console.log('Submitting:', data)
}
</script>
Best practices
- Keep form state in the parent if possible.
- Use dynamic field configs for flexibility.
- Use computed properties for validation when possible.
- Use slots for special UI cases.
- Consider form libraries like:
- VeeValidate
- FormKit
- Vuetify/Element Plus form components if using a UI framework
If your form is complex
If you have:
- nested fields
- async validation
- repeatable groups
- multi-step forms
then it’s often better to split into smaller reusable components:
BaseInput.vueBaseSelect.vueFormSection.vueReusableForm.vue
If you want, I can also show:
- a dynamic form builder in Vue 3, or
- a version using VeeValidate.