Next.js
GolemUI renders on the server in the Next.js App Router with the same packages and the same <GuiForm> component as a client-only React app. Verified with Next.js 15 and React 19. The contract the adapter follows is on the React SSR page; this page is the wiring. Requires GolemUI 1.5.0 or newer.
Quick start
Section titled “Quick start”npx degit golemui/golemui/templates/nextjs my-appcd my-appnpm installnpm startOpen http://localhost:3000 and view the page source: the form markup arrives without JavaScript. The same template runs in StackBlitz. Node.js 18.18 or newer.
Installation
Section titled “Installation”npm i @golemui/core @golemui/gui-shared @golemui/react @golemui/gui-react @golemui/gui-components --savenext.config.ts needs nothing GolemUI-specific.
Preload the widgets
Section titled “Preload the widgets”The widgets have to be in the registry before the first render, on the server and on the client. A server render cannot wait for a widget’s dynamic import while it produces the page, and the client would otherwise hydrate an empty tree over markup that is not empty.
In Next.js that is a client component that suspends on a module-scope promise. The promise lives at module scope, so there is one per environment, and use() suspends the first render until it resolves: the server waits before emitting HTML, the client waits before hydrating.
'use client';
import { preloadFormWidgets } from '@golemui/core';import { widgetLoaders } from '@golemui/gui-react';import { use, type ReactNode } from 'react';
const preloadPromise = preloadFormWidgets({ widgetLoaders });
export function GolemuiProvider({ children }: { children: ReactNode }) { use(preloadPromise); return <>{children}</>;}Root layout
Section titled “Root layout”Wrap the app in the provider and load the stylesheet once. GolemUI never injects it. data-theme="auto" follows the OS color scheme.
import type { ReactNode } from 'react';import { GolemuiProvider } from './golemui-provider';import '@golemui/gui-components/index.css';import './styles.css';
export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="en" data-theme="auto"> <body> <GolemuiProvider>{children}</GolemuiProvider> </body> </html> );}Render the form
Section titled “Render the form”A page that renders <GuiForm> is a client component: the config holds functions. Keep the config at module scope, because a new identity per render re-initializes the form.
'use client';
import type { FormEvent } from '@golemui/core';import { GuiForm } from '@golemui/gui-react';import { gui, type GuiFormInitConfig } from '@golemui/gui-shared';
const formDef = [ gui.inputs.dropdown('country', { label: 'Country', items: ['United States', 'Japan', 'Brazil', 'France'], }), gui.inputs.checkbox('pets', { label: 'Travelling with pets?', include: { when: '!!$form.country' }, }), gui.actions.button({ label: 'Submit', uid: 'submit', onClick: () => 'handleSubmit' }),];
const config: GuiFormInitConfig = { formName: 'signup', // keeps the form id predictable on both sides formDef,};
function handleFormEvent(event: FormEvent) { if (event.name === 'handleSubmit') console.log('Submitted:', event.data);}
export default function Page() { return <GuiForm config={config} formEvent={handleFormEvent} />;}'use client';
import type { FormEvent } from '@golemui/core';import { GuiForm } from '@golemui/gui-react';import formDef from './signup.json';
const config = { formName: 'signup', // keeps the form id predictable on both sides formDef,};
function handleFormEvent(event: FormEvent) { if (event.name === 'handleSubmit') console.log('Submitted:', event.data);}
export default function Page() { return <GuiForm config={config} formEvent={handleFormEvent} />;}Render the widgets on the server
Section titled “Render the widgets on the server”The gui-* elements can also render their content on the server, so labels, inputs and values are in the HTML. It is optional. The fields are complete in the HTML before any JavaScript runs, and the page doesn’t change when the elements upgrade; in exchange, the HTML is bigger and the server does more work on each request. Without it, the page stays as described on the React SSR page.
To turn it on, install @lit-labs/ssr and import the server entry point of GolemUI Components in the root layout, which is a Server Component:
import '@golemui/gui-components/ssr';Import it in the root layout rather than instrumentation.ts, which does not run when the build prerenders static pages. What the elements render on the server, and its limits, are in Server rendering in the GolemUI Components docs.
- Pages and components that render
<GuiForm>are client components ('use client'). - Keep
configat module scope. formNameis optional on React, sinceuseIdmatches on both sides. Set it when the form id in the markup must be predictable.- Handlers never run on the server.
onLoadfires once the widget has mounted in the browser. - Segment config such as
export const dynamic = 'force-dynamic'only works in a server component file. Put it in a thin server page that renders the'use client'implementation. - A raw
gui-*tag placed directly in JSX takes object props as properties in React 19 (<gui-select options={list} onChange={…} />). It gets nodefer-hydration, so render it after mount, or it upgrades before hydration and React reports a mismatch.
Custom widgets
Section titled “Custom widgets”Keep the loaders in one module-scope object, spread it into the provider’s preload call and pass the same object as customWidgetLoaders in every form config. See Custom widget loaders.
import { customWidgetLoaders } from '../widgets';
const preloadPromise = preloadFormWidgets({ widgetLoaders: { ...widgetLoaders, ...customWidgetLoaders },});See also
Section titled “See also”- React SSR: how the adapter renders and hydrates, and what the server sends.
- Server-Side Rendering Overview: the rules shared by every framework.
- React Integration: the full
<GuiForm>props reference.