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.
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
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
export default function ContactPage() {
return (
<main>
<h1>Contact us</h1>
<ContactForm />
</main>
)
}
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 })
}
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:
// 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:
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} />
}
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