Skip to main content
FormEngine works with Next.js App Router and Pages Router. Because FormEngine renders on the client side, you need to disable SSR for any component that uses FormViewer or FormBuilder.
A complete working example is available in the FormEngine GitHub repository.

Prerequisites

  • Next.js 13.4+ (App Router) or Next.js 12+ (Pages Router)
  • @react-form-builder/core installed
  • A component library adapter (e.g. @react-form-builder/components-material-ui)
Next.js 12 has a bug that incorrectly minifies JavaScript containing the ?? operator. Either disable swcMinify in next.config.js or upgrade to Next.js 13+.

App Router setup

1. Install packages

npm install @react-form-builder/core @react-form-builder/components-material-ui @emotion/react @emotion/styled

2. Create a client form component

FormEngine requires the browser DOM, so any component using FormViewer must be a Client Component. Create a dedicated file:
app/components/ContactForm.tsx
'use client'


const formSchema = {
  errorType: 'MuiErrorWrapper',
  tooltipType: 'MuiTooltip',
  form: {
    key: 'Screen',
    type: 'Screen',
    children: [
      {
        key: 'name',
        type: 'MuiTextField',
        props: { label: { value: 'Full name' } },
        schema: { validations: [{ key: 'required' }] }
      },
      {
        key: 'email',
        type: 'MuiTextField',
        props: { label: { value: 'Email address' } },
        schema: {
          validations: [
            { key: 'required' },
            { key: 'email' }
          ]
        }
      },
      {
        key: 'submitButton',
        type: 'MuiButton',
        props: {
          children: { value: 'Send' },
          variant: { value: 'contained' }
        },
        events: {
          onClick: [
            { name: 'validate', type: 'common', args: { failOnError: true } },
            { name: 'onSubmit', type: 'custom' }
          ]
        }
      }
    ]
  }
}

export default function ContactForm() {
  const getForm = useCallback(() => JSON.stringify(formSchema), [])

  const actions = useMemo(() => ({
    onSubmit: async (event: any) => {
      const response = await fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(event.data),
      })
      if (response.ok) {
        alert('Message sent!')
      }
    }
  }), [])

  return (
    <FormViewer
      view={muiView}
      getForm={getForm}
      actions={actions}
    />
  )
}

3. Use in a Server Component page

app/contact/page.tsx

export default function ContactPage() {
  return (
    <main>
      <h1>Contact us</h1>
      <ContactForm />
    </main>
  )
}

4. Add an API route for form submission

app/api/contact/route.ts

export async function POST(request: NextRequest) {
  const data = await request.json()

  // Process form data — save to DB, send email, etc.
  console.log('Form submission:', data)

  return NextResponse.json({ success: true })
}

Loading form schema from the server

A key advantage of FormEngine is that form schemas can live in a database and be loaded at runtime. Here’s how to combine Next.js server-side data fetching with FormEngine’s client-side rendering:
app/forms/[id]/page.tsx
// Server Component — fetches the schema
async function getFormSchema(id: string) {
  const res = await fetch(`https://your-api.com/forms/${id}`, {
    cache: 'no-store' // or 'force-cache' for static forms
  })
  return res.json()
}

export default async function FormPage({ params }: { params: { id: string } }) {
  const schema = await getFormSchema(params.id)
  return <DynamicFormViewer initialSchema={schema} />
}
app/components/DynamicFormViewer.tsx
'use client'


export default function DynamicFormViewer({ initialSchema }: { initialSchema: object }) {
  const getForm = useCallback(
    () => JSON.stringify(initialSchema),
    [initialSchema]
  )

  return <FormViewer view={muiView} getForm={getForm} />
}

Alternative: dynamic import with SSR disabled

If you prefer not to use the 'use client' directive (for example, in an older codebase), use next/dynamic to disable SSR on the import:

const FormViewer = dynamic(
  () => import('@react-form-builder/core').then(mod => mod.FormViewer),
  { ssr: false }
)
This approach works but adds a loading waterfall — the 'use client' component approach is preferred in App Router projects.

Pages Router setup

For the Pages Router, the same rules apply — FormEngine needs client-side only rendering:
pages/contact.tsx

const FormViewer = dynamic(
  () => import('@react-form-builder/core').then(mod => mod.FormViewer),
  { ssr: false }
)

export default function ContactPage() {
  const getForm = useCallback(() => JSON.stringify(formSchema), [])

  const actions = useMemo(() => ({
    onSubmit: async (event: any) => {
      await fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(event.data),
      })
    }
  }), [])

  return <FormViewer view={muiView} getForm={getForm} actions={actions} />
}

Using FormEngine Designer with Next.js

The same 'use client' requirement applies to FormBuilder:
app/admin/form-editor/page.tsx
'use client'


export default function FormEditorPage() {
  return <FormBuilder view={builderViewWithCss} />
}
See Designer overview for full setup including form storage and persistence.

Troubleshooting

“window is not defined” error This means FormEngine is being rendered on the server. Add 'use client' to the component file, or use the dynamic import with { ssr: false }. Hydration mismatch warnings These usually appear when the initialData prop has values that differ between server render and client hydration. Since FormEngine is client-only, this is typically resolved by the 'use client' directive — you shouldn’t see hydration issues. Module resolution errors after upgrade Clear .next cache and reinstall: rm -rf .next node_modules && npm install
Last modified on April 16, 2026