Skip to content

Watch ​

React to form field value changes in real-time.

Basic Usage ​

Use watch from useForm to observe field values:

vue
<script setup>
import { useForm } from '@vuehookform/core'

const { register, watch } = useForm({ schema })

// Watch single field
const email = watch('email')

// Watch multiple fields
const credentials = watch(['email', 'password'])

// Watch all fields
const allValues = watch()
</script>

<template>
  <input v-bind="register('email')" />
  <p>Current email: {{ email }}</p>
</template>

Watch Modes ​

Single Field ​

typescript
const email = watch('email')
// Returns: ComputedRef<string>

// Use in template or computed
console.log(email.value)

Multiple Fields ​

typescript
const credentials = watch(['email', 'password'])
// Returns: ComputedRef<{ email?: string, password?: string }>

// Access individual values via the object
console.log(credentials.value.email, credentials.value.password)

All Fields ​

typescript
const allValues = watch()
// Returns: ComputedRef<FormValues>

// Access any field
console.log(allValues.value.email)

Common Use Cases ​

Live Preview ​

vue
<script setup>
import { useForm } from '@vuehookform/core'

const { register, handleSubmit, watch } = useForm({ schema })
const formData = watch()
</script>

<template>
  <div class="editor">
    <form @submit="handleSubmit(onSubmit)">
      <input v-bind="register('title')" placeholder="Title" />
      <textarea v-bind="register('content')" placeholder="Content"></textarea>
    </form>

    <div class="preview">
      <h1>{{ formData.title || 'Untitled' }}</h1>
      <p>{{ formData.content || 'Start writing...' }}</p>
    </div>
  </div>
</template>

Computed Values ​

vue
<script setup>
import { computed } from 'vue'
import { useForm } from '@vuehookform/core'

const { register, watch } = useForm({ schema })

const quantity = watch('quantity')
const price = watch('price')

const total = computed(() => {
  return (quantity.value || 0) * (price.value || 0)
})

const formattedTotal = computed(() => {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
  }).format(total.value)
})
</script>

<template>
  <input v-bind="register('quantity')" type="number" />
  <input v-bind="register('price')" type="number" step="0.01" />
  <p>Total: {{ formattedTotal }}</p>
</template>

Conditional Logic ​

vue
<script setup>
import { useForm } from '@vuehookform/core'

const { register, watch } = useForm({ schema })
const shippingMethod = watch('shippingMethod')
</script>

<template>
  <select v-bind="register('shippingMethod')">
    <option value="standard">Standard</option>
    <option value="express">Express</option>
    <option value="pickup">Store Pickup</option>
  </select>

  <!-- Show address fields only for delivery -->
  <div v-if="shippingMethod !== 'pickup'">
    <input v-bind="register('address')" placeholder="Address" />
    <input v-bind="register('city')" placeholder="City" />
  </div>

  <!-- Show store selection for pickup -->
  <div v-else>
    <select v-bind="register('storeId')">
      <option value="">Select a store</option>
      <!-- Store options -->
    </select>
  </div>
</template>

Character Counter ​

vue
<script setup>
import { computed } from 'vue'
import { useForm } from '@vuehookform/core'

const { register, watch } = useForm({ schema })
const bio = watch('bio')

const charCount = computed(() => bio.value?.length || 0)
const maxChars = 500
const remaining = computed(() => maxChars - charCount.value)
</script>

<template>
  <div class="bio-field">
    <label>Bio</label>
    <textarea v-bind="register('bio')" :maxlength="maxChars"></textarea>
    <span :class="{ warning: remaining < 50 }"> {{ remaining }} characters remaining </span>
  </div>
</template>

Dependent Fields ​

vue
<script setup>
import { watchEffect } from 'vue'
import { useForm } from '@vuehookform/core'

const { register, watch, setValue } = useForm({ schema })
const country = watch('country')

// Update available states when country changes
const availableStates = computed(() => {
  return getStatesForCountry(country.value)
})

// Clear state when country changes
watchEffect(() => {
  if (country.value) {
    setValue('state', '')
  }
})
</script>

Form Dirty State Indicator ​

vue
<script setup>
import { computed } from 'vue'
import { useForm } from '@vuehookform/core'

const { register, watch } = useForm({
  schema,
  defaultValues: {
    name: 'Original Name',
    email: 'original@email.com',
  },
})

const defaultValues = {
  name: 'Original Name',
  email: 'original@email.com',
}

const currentValues = watch()

const hasChanges = computed(() => {
  return JSON.stringify(currentValues.value) !== JSON.stringify(defaultValues)
})
</script>

<template>
  <div>
    <span v-if="hasChanges" class="unsaved-indicator"> Unsaved changes </span>
    <form>
      <!-- fields -->
    </form>
  </div>
</template>

useWatch Composable ​

For child components, use the useWatch composable:

vue
<!-- PasswordStrength.vue -->
<script setup>
import { computed } from 'vue'
import { useWatch } from '@vuehookform/core'

const props = defineProps<{
  control: Control<any>
}>()

const password = useWatch({
  control: props.control,
  name: 'password',
  defaultValue: '',
})

const strength = computed(() => {
  const p = password.value
  if (!p) return { level: 0, label: 'None' }
  if (p.length < 8) return { level: 1, label: 'Weak' }
  if (/[A-Z]/.test(p) && /[0-9]/.test(p)) return { level: 3, label: 'Strong' }
  return { level: 2, label: 'Medium' }
})
</script>

<template>
  <div class="password-strength">
    <div class="strength-bar" :data-level="strength.level" />
    <span>{{ strength.label }}</span>
  </div>
</template>

Usage:

vue
<script setup>
import { useForm } from '@vuehookform/core'
import PasswordStrength from './PasswordStrength.vue'

const { control, register } = useForm({ schema })
</script>

<template>
  <input v-bind="register('password')" type="password" />
  <PasswordStrength :control="control" />
</template>

Performance Tips ​

  1. Watch specific fields instead of all values when possible
  2. Use computed properties for derived values to leverage Vue's caching
  3. Avoid expensive operations in watch callbacks
  4. Consider debouncing for real-time features like auto-save
typescript
import { watchDebounced } from '@vueuse/core'

const formData = watch()

watchDebounced(
  formData,
  (values) => {
    autoSave(values)
  },
  { debounce: 1000 },
)

Next Steps ​

Released under the MIT License.