GitHub

Pinia & Composables

Share form instances across components using Pinia stores or dedicated composables.

A form instance is a plain reactive object. You can store it in a Pinia store, return it from a composable, or pass it around like any other reactive value.

Scoped composable

The simplest pattern. One composable creates and owns an instance for a specific feature. Any component that calls it gets the same state when the composable is called within the same scope.

import { z } from 'zod'

const schema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  message: z.string().min(10),
})

const form = useNotForm({
  schema,
  onSubmit: async (values) => {
    await $fetch('/api/contact', { method: 'POST', body: values })
  },
})

export function useContactForm() {
  return { form }
}

Pinia store

Store the instance in a Pinia store for global access across a complex feature.

import { defineStore } from 'pinia'
import { z } from 'zod'

const schema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  password: z.string().min(8),
})

export const useSignupStore = defineStore('signup', () => {
  const form = useNotForm({
    schema,
    initialValues: { name: '', email: '', password: '' },
    onSubmit: async (values) => {
      await $fetch('/api/signup', { method: 'POST', body: values })
      await navigateTo('/dashboard')
    },
  })

  return { form }
})
useNotForm uses reactive() internally. Pinia does not interfere with the arrays and Sets inside the instance. Values, errors, touchedFields, and dirtyFields remain fully reactive. The instance itself is wrapped with markRaw to prevent Pinia from making it deeply reactive unnecessarily.

Reading state outside components

The instance is plain reactive — read it in watchers, computed values, or other stores.

const store = useSignupStore()

// Watch for any dirty field
watch(() => store.form.isDirty.value, (dirty) => {
  if (dirty) enableDraftSave()
})

// Check validity without running validation
if (store.form.isValid.value) {
  proceedToPayment()
}