Skip to content

Troubleshooting ​

Common issues and their solutions when working with Vue Hook Form.

Common Mistakes ​

Field Arrays ​

Most field array issues stem from violating critical rules. See Field Arrays - Critical Rules for:

  • Using dot notation for paths (not bracket notation)
  • Using field.key for v-for keys (not index)
  • Initializing arrays in defaultValues
  • Calling fields() in setup (not in template)

Forgetting .value on Refs ​

Form state and watched values are Vue refs - access them with .value:

vue
<!-- CORRECT -->
<span v-if="formState.value.errors.email">
  {{ formState.value.errors.email }}
</span>

<!-- WRONG - this won't work -->
<span v-if="formState.errors.email">
  {{ formState.errors.email }}
</span>

Mixing v-model with Uncontrolled Register ​

Don't use v-model with the default uncontrolled mode:

vue
<!-- CORRECT - uncontrolled mode -->
<input v-bind="register('email')" />

<!-- CORRECT - controlled mode with v-model -->
<script setup>
const { value: email, ...bindings } = register('email', { controlled: true })
</script>
<input v-model="email" v-bind="bindings" />

<!-- WRONG - v-model with uncontrolled register -->
<input v-model="email" v-bind="register('email')" />

Frequently Asked Questions ​

Why isn't my field updating? ​

Possible causes:

  1. Using uncontrolled mode with reactive data: Uncontrolled inputs read from/write to the DOM directly. If you need reactive updates, use controlled mode:

    typescript
    const { value, ...bindings } = register('field', { controlled: true })
  2. Not calling getValues() before accessing values: For uncontrolled inputs, use getValues() to sync DOM state:

    typescript
    const currentValues = getValues() // Syncs from DOM
  3. Missing ref on custom component: Ensure your component forwards the ref:

    vue
    <!-- CustomInput.vue -->
    <template>
      <input ref="inputRef" v-bind="$attrs" />
    </template>
    <script setup>
    import { ref } from 'vue'
    const inputRef = ref()
    defineExpose({ focus: () => inputRef.value?.focus() })
    </script>

Why do I get validation errors immediately? ​

Check your validation mode:

typescript
const { register } = useForm({
  schema,
  mode: 'onSubmit', // Only validate on submit (default)
  // mode: 'onChange', // Validates on every change - may feel aggressive
  // mode: 'onBlur', // Validates when field loses focus
  // mode: 'onTouched', // Validates after first touch, then on change
})

Using onChange? Consider using delayError to prevent error flash during typing:

typescript
const { register } = useForm({
  schema,
  mode: 'onChange',
  delayError: 500, // Wait 500ms before showing errors
})

Why is my form slow? ​

Common performance issues:

  1. Watching all fields unnecessarily:

    typescript
    // SLOW - re-renders on every field change
    const allValues = watch()
    
    // FAST - only watch what you need
    const email = watch('email')
  2. Using controlled mode everywhere: Controlled mode uses Vue reactivity which has overhead. Use uncontrolled (default) for simple inputs.

  3. Large field arrays without virtualization: For 100+ items, consider virtual scrolling:

    vue
    <VirtualList :items="items.value" :item-height="50">
      <template #default="{ item }">
        <input v-bind="register(`items.${item.index}.name`)" />
      </template>
    </VirtualList>
  4. Complex schemas validated on every change: Use mode: 'onBlur' or mode: 'onSubmit' for complex forms.

How do I handle server errors? ​

Option 1: Using setError after submission

typescript
async function onSubmit(data) {
  try {
    await api.save(data)
  } catch (error) {
    if (error.field) {
      setError(error.field, { message: error.message })
    } else {
      setError('root', { message: 'Submission failed' })
    }
  }
}

Option 2: Using the errors option

typescript
const serverErrors = ref({})

const { register } = useForm({
  schema,
  errors: serverErrors, // Merged with validation errors
})

// After API call fails:
serverErrors.value = { email: 'Email already exists' }

How do I reset to different values? ​

typescript
// Reset to original defaults
reset()

// Reset to new values
reset({
  name: 'New Name',
  email: 'new@email.com',
})

// Reset but keep some state
reset(undefined, {
  keepErrors: true,
  keepDirty: true,
})

How do I validate a single field manually? ​

typescript
// Validate single field
const isValid = await trigger('email')

// Validate multiple fields
const areValid = await trigger(['email', 'password'])

// Validate entire form
const formValid = await trigger()

How do I access the current value of a field? ​

typescript
// Option 1: getValues (syncs from DOM for uncontrolled)
const email = getValues('email')

// Option 2: watch (reactive, for use in template/computed)
const email = watch('email')
console.log(email.value)

// Option 3: Controlled mode (reactive binding)
const { value: email } = register('email', { controlled: true })

Can I use uncontrolled mode with PrimeVue/Vuetify? ​

Yes, for many components! Vue Hook Form automatically detects input elements inside Vue components.

Works automatically:

  • Components where $el is the input (PrimeVue InputText, InputNumber)
  • Components where $el is a wrapper containing an <input>, <select>, or <textarea>
vue
<!-- This works with uncontrolled mode -->
<InputText v-bind="register('username')" />

Use controlled mode instead for:

  • Components with complex structures (multiple inputs, custom rendering)
  • Components that don't use standard input events (some date pickers, autocompletes)
  • When you need reactive value access during typing

To check if a component is compatible:

javascript
// In browser DevTools console, find your component instance
const vm = document.querySelector('.p-inputtext').__vueParentComponent?.proxy
console.log(vm?.$el) // Should be an <input> or contain one

See Vue Component Library Support for more details.

Why aren't my array operations working? ​

Array methods return false if the operation was rejected:

typescript
const items = fields('items', {
  rules: { maxLength: { value: 5, message: 'Max 5 items' } },
})

const success = items.append({ name: '' })
if (!success) {
  console.log('Could not add item - max length reached')
}

Check return values and ensure you're within min/max constraints.

Debugging Tips ​

Enable Vue DevTools ​

Vue DevTools shows reactive state. Look for:

  • formState ref values
  • watch() computed values
  • Component re-renders

Log Form State ​

typescript
import { watchEffect } from 'vue'

// Log all state changes
watchEffect(() => {
  console.log('Form state:', {
    errors: formState.value.errors,
    isDirty: formState.value.isDirty,
    isValid: formState.value.isValid,
  })
})

Check Field Registration ​

typescript
// After registering, verify the field is tracked
const bindings = register('email')
console.log('Registered field:', bindings)

Inspect Zod Schema ​

typescript
// Validate data manually to see all errors
const result = schema.safeParse(formData)
if (!result.success) {
  console.log('Validation errors:', result.error.format())
}

TypeScript Issues ​

Path Autocomplete Not Working ​

Ensure your schema type is inferred correctly:

typescript
// CORRECT - type is inferred
const schema = z.object({
  email: z.string().email(),
})
const { register } = useForm({ schema })
register('email') // Autocomplete works

// ISSUE - schema as const may lose type info
const schema = { ... } as const // May not work as expected

Type Error on Dynamic Paths ​

For dynamic paths (like array indices), you may need type assertions:

typescript
// If TypeScript complains about dynamic paths:
register(`items.${index}.name` as `items.${number}.name`)

// Or use a typed helper
function itemPath(index: number, field: string) {
  return `items.${index}.${field}` as const
}
register(itemPath(0, 'name'))

Generic Component Types ​

For reusable components, use generics:

typescript
import type { ZodType } from 'zod'
import type { Control, FormPath } from '@vuehookform/core'

const props = defineProps<{
  control: Control<ZodType>
  name: FormPath<typeof props.control>
}>()

Getting Help ​

If you're still stuck:

  1. Check the API Reference for detailed method signatures
  2. Look at Examples for working code
  3. Search or open an issue on GitHub

Next Steps ​

Released under the MIT License.