---
url: https://formhaus.dev/guide/ssr.md
description: >-
  Render Formhaus forms on the server with Next.js App Router or Nuxt and
  hydrate them without mismatches. Load Cloud forms on the server with
  initialForm.
---

# Server rendering

`FormRenderer` and `FormhausForm` render on the server and hydrate on the client without mismatches.

## What runs where

* `@formhaus/core` does not touch the DOM. The engine, `validateSubmission()` and `fetchForm()` run in Node, server components and route handlers.
* Field ids come from React `useId` and Vue `useId`. On Vue 3.3 and 3.4 the ids come from the component's position in the app, so the server and the client get the same ids.
* Hidden fields with a `source` render their `defaultValue` on the server. Page values such as the query string, referrer, language and time zone are read after hydration.
* Focus, the bot check and draft restore run after mount.

## Next.js App Router

The server component loads the definition. `FormRenderer` takes an `onSubmit` function, so it goes in a `'use client'` component:

```tsx
import { definition } from '@/form/definition';
import { SignupForm } from './signup-form';

export default function Page() {
  return (
    <main>
      <h1>{definition.title}</h1>
      <SignupForm definition={definition} />
    </main>
  );
}

```

```tsx
'use client';

import type { FormDefinition } from '@formhaus/core';
import { FormRenderer } from '@formhaus/react';
import { useState } from 'react';

type SubmitResponse = { values: Record<string, unknown> } | { errors: Record<string, string> };

export function SignupForm({ definition }: { definition: FormDefinition }) {
  const [errors, setErrors] = useState<Record<string, string>>();
  const [saved, setSaved] = useState<Record<string, unknown> | null>(null);

  async function submit(values: Record<string, unknown>, skippedSteps: string[]) {
    const response = await fetch('/api/submit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ values, skippedSteps }),
    });
    const result = (await response.json()) as SubmitResponse;
    if ('errors' in result) setErrors(result.errors);
    if ('values' in result) setSaved(result.values);
  }

  if (saved) return <pre>{JSON.stringify(saved, null, 2)}</pre>;

  return <FormRenderer definition={definition} errors={errors} onSubmit={submit} />;
}

```

The definition can also come from a database or an API. Fetch it in the server component and pass it as a prop. Full example with the route handler: [Next.js App Router](/guide/integrations/nextjs).

## Nuxt

Nuxt renders pages on the server by default. `FormRenderer` needs no wrapper:

```vue
<script setup lang="ts">
import { FormRenderer } from '@formhaus/vue';
import { definition, type SubmitResponse } from '~~/shared/definition';

const errors = ref<Record<string, string>>();
const saved = ref<Record<string, unknown> | null>(null);

async function submit(values: Record<string, unknown>, skippedSteps: string[]) {
  const result = await $fetch<SubmitResponse>('/api/submit', {
    method: 'POST',
    body: { values, skippedSteps },
    ignoreResponseError: true,
  });
  if ('errors' in result) errors.value = result.errors;
  if ('values' in result) saved.value = result.values;
}
</script>

<template>
  <main>
    <h1>{{ definition.title }}</h1>
    <pre v-if="saved">{{ JSON.stringify(saved, null, 2) }}</pre>
    <FormRenderer v-else :definition="definition" :errors="errors" @submit="submit" />
  </main>
</template>

```

Full example with the server route: [Nuxt](/guide/integrations/nuxt).

## Cloud forms

Without `initialForm`, `FormhausForm` renders its `fallback` on the server and loads the form in the browser. Load the form on the server with `fetchForm()` and pass it as `initialForm`. The server HTML then holds the whole form, and the browser makes no request for it.

Next.js: `@formhaus/react/cloud` is a client module, so a server component renders `FormhausForm` directly:

```tsx
import { fetchForm } from '@formhaus/core/cloud';
import { FormhausForm } from '@formhaus/react/cloud';

export const revalidate = 60;

export default async function Page() {
  const form = await fetchForm({ id: 'YOUR_FORM_ID' }).catch(() => undefined);
  return <FormhausForm id="YOUR_FORM_ID" initialForm={form} />;
}

```

Nuxt: `useAsyncData` sends the loaded form to the client with the page payload:

```vue
<script setup lang="ts">
import { fetchForm } from '@formhaus/core/cloud';
import { FormhausForm } from '@formhaus/vue/cloud';

const { data: form } = await useAsyncData('contact-form', () => fetchForm({ id: 'YOUR_FORM_ID' }).catch(() => null));
</script>

<template>
  <FormhausForm id="YOUR_FORM_ID" :initial-form="form ?? undefined" />
</template>

```

If the load fails, `initialForm` is empty and the form loads in the browser. `revalidate` keeps a cached Next.js page within a minute of the published version.

## What to avoid

* Reading `window`, `document`, `localStorage` or the current time in a custom field component during render. Read them in `useEffect` or `onMounted`.
* `initialValues` that differ between the server and the client, for example values from `localStorage`. Pass the same values on both sides, or set them after mount with the [`setValue` handle](/guide/#definition-and-initial-values).
* Formatting dates or numbers with the default locale in a custom component. The server locale can differ from the browser locale. Pass an explicit locale.
* Two server-rendered Vue apps on one page. Give each app its own `app.config.idPrefix`, so the field ids do not collide.
* On Vue 3.3 and 3.4, a form inside an async component or `<Suspense>` hydrates late, and the field ids of the other forms in the same app shift. Mount such a form as its own app with its own `idPrefix`, or use Vue 3.5.
