SSR hydration: Nuxt
The fastest path to server-rendered form values that don't flicker on hydrate. Install the module, write
useFormnormally, and the round-trip wires itself.
- Category
- Integration
- Setup
modules: [attaform/nuxt]- Transport
- nuxtApp.payload (top-level key)
- What rides
- values · errors · field flags
This page is code-only; SSR happens at the server runtime, and the docs site can't demo a hydration round-trip without bootstrapping a separate server. The setup is small enough to verify in your own Nuxt project; see SSR hydration: bare Vue for the equivalent without the Nuxt convenience.
Nothing to wire
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['attaform/nuxt'],
})
<script setup lang="ts">
const form = useForm({ schema, key: 'signup' })
</script>
That's the whole setup. Values, errors, and field interaction flags (touched / focused / blurred / connected) survive the server → client round-trip through nuxtApp.payload. Need to peek? Open the rendered HTML and look for the Nuxt payload <script> block; attaform is a top-level key.
What crosses the wire
| Surface | Round-trips? | Notes |
|---|---|---|
form.values | ✅ | Whole tree, including nested objects and arrays. |
errors | ✅ | Every entry in the error map, keyed by path. |
fields | ✅ | touched / focused / blurred / connected / updatedAt per path. |
blankPaths | ✅ | Numeric-blank state survives the boundary. |
history chain | ❌ | Each tab walks its own undo timeline. |
| Validation in-flight state | ❌ | Re-runs locally on the client. |
The client-side form is identical to the server one; no second round of validation kicks in unless the user interacts.
Auto-imports
The Nuxt module auto-imports useForm from the unified entry point. No import statement needed in <script setup>:
<script setup lang="ts">
const form = useForm({ schema, key: 'signup' })
</script>
For the Zod-typed wrapper (useForm from attaform/zod), or for any other helper (injectForm, useWizard, useRegister, parseApiErrors, unset, defaultDisplayState), import explicitly:
<script setup lang="ts">
import { injectForm, useWizard } from 'attaform/zod'
import { parseApiErrors } from 'attaform'
</script>
App-wide defaults under Nuxt
The Nuxt module surfaces the same AttaformDefaults you'd pass to createAttaform({ defaults }):
export default defineNuxtConfig({
modules: ['attaform/nuxt'],
attaform: {
defaults: {
validateOn: 'change',
debounceMs: 100,
onInvalidSubmit: 'focus-first-error',
},
},
})
See App-wide defaults for the full option list and merge semantics.
Common issues
"The form is empty on the client even though the server rendered values."
- Does the form's
keymatch between server and client? Hard-code it as a string literal;uuidv4()orMath.random()produces a fresh key per render and breaks the round-trip lookup. - Was the form created in
setup? Forms created inonMountedor event handlers aren't in the SSR snapshot.
"Field errors from the server disappear on first interaction."
By design. Any mutation re-runs validation, which can replace the errors. To keep server-provided errors around until the user dirties the field, gate the display on form.fields.<path>.touched or form.meta.dirty:
<small v-if="form.errors.email?.[0] && !form.fields.email.touched">
{{ form.errors.email[0].message }}
</small>
"Some fields look right, others don't."
Forms created in onMounted or event handlers aren't in the SSR snapshot. Create forms during setup so the server sees them.
DevTools panel
The Nuxt module auto-wires the Attaform DevTools panel into the Nuxt DevTools sidebar. The panel inspects every registered form including the SSR-rendered ones, useful for confirming the server-rendered shape matches your expectation before the user touches anything.
Where to next
- SSR hydration: bare Vue: the same round-trip without the Nuxt module, when you're not on Nuxt.
- Persistence overview: different problem (durable drafts vs. one-time SSR snapshot), composable with SSR.
- Performance: what SSR hydration costs in practice.