Visual Editor
Live-editing patterns for Next.js — LocalessDocument, the useLocaless hook, and how the sync script gets loaded.
Visual Editor
With useLocaless Hook
When enableSync: true is set in localessInit, the useLocaless hook handles the full cycle automatically — initial fetch and live sync updates — with no extra wiring needed.
'use client';
import { useLocaless, LocalessComponent, localessEditable } from "@localess/react";
import type { Page } from "./.localess/localess";
export function PageView({ slug, locale }: { slug: string; locale?: string }) {
const content = useLocaless<Page>(slug, { locale });
if (!content) return null;
return (
<main {...localessEditable(content.data)}>
{content.data?.body.map(item => (
<LocalessComponent key={item._id} data={item} links={content.links} references={content.references} />
))}
</main>
);
}With LocalessDocument Component
LocalessDocument is a component alternative to the hook. Pass it the full server-fetched content response and it handles live sync updates internally, delegating rendering to LocalessComponent.
// app/[locale]/page.tsx (Server Component — fetches data)
import { getLocalessClient, LocalessDocument } from "@localess/react/rsc";
import type { Page } from "./.localess/localess";
export default async function HomePage({ params }: { params: Promise<{ locale?: string }> }) {
const { locale } = await params;
const client = getLocalessClient();
const content = await client.getContentBySlug<Page>('home', { locale });
return <LocalessDocument document={content} />;
}Props:
| Prop | Type | Required | Description |
|---|---|---|---|
document | Content<T> | Yes | Full content response object (from getContentBySlug/getContentById) |
ref | React.Ref<HTMLElement> | No | Forwarded to the rendered root element |
LocalessDocumentsubscribes toinput/changeeditor events automatically whenenableSyncis active.Import it from
@localess/react/rsc(as above) when calling it directly from a Server Component, as this example does — that variant is a Server Component whose live sync is driven by a Server Action, with no client-side component registration needed:localessInit({ components })once, server-side, is enough. The plain@localess/reactexport'sLocalessDocumentis a Client Component internally; calling it directly from a Server Component moves that lookup into a separate client module graph where the registration set by a Server Component'slocalessInit()call was never applied, and it fails to find any registered component. Use the plain@localess/reactexport'sLocalessDocumentonly from inside an actual'use client'file (see the SPA example below).Known limitation: the
/rscLocalessDocument's live-edit cache is in-process memory. On adefault-mode deployment running multiple serverless instances with no shared memory, a live edit may occasionally not appear until a subsequent edit lands on the same instance. This doesn't affectstandalonedeployments or local development. Requires a live server at request time — does not work underoutput: 'export'; use the client-side fallback below instead.See the
playgrounds/nextplayground for a full working Next.js App Router + RSC project built on this pattern.
Static export fallback
When your Next.js app uses output: 'export', there's no server at request time to drive the /rsc LocalessDocument's Server Action. Use the default export's LocalessDocument (@localess/react) instead — a Client Component holding its own state and re-rendering on window.localess input/change events.
Earlier releases re-exported this from
/rscunder the aliasLocalessClientDocument. That alias no longer exists — importLocalessDocumentfrom@localess/reactdirectly.
Because Next.js App Router bundles Server and Client Components into separate module graphs, a localessInit({ enableSync: true, components }) call made only in a Server Component populates neither the component registry nor the enableSync flag in the Client Component module graph this fallback actually runs in. Call localessInit a second time, from inside the Client Component boundary, using a public token — read-only, scoped to published content and translations only, safe to expose client-side (unlike the secret token used for the server-side call):
// app/[locale]/page-client.tsx
'use client';
import { localessInit, LocalessDocument } from "@localess/react";
import { components } from "@/localess.config"; // the same map passed to localessInit server-side
localessInit({
origin: process.env.NEXT_PUBLIC_LOCALESS_ORIGIN!, // same origin as the server-side call
spaceId: process.env.NEXT_PUBLIC_LOCALESS_SPACE_ID!, // same spaceId as the server-side call
token: process.env.NEXT_PUBLIC_LOCALESS_PUBLIC_TOKEN!, // a public token — never the secret one
enableSync: true,
components,
});
export default function PageClient({ content }) {
return <LocalessDocument document={content} />;
}This is a plain second call to the same localessInit you already use server-side — no separate function to learn. The server-side call keeps using the secret token (for build-time data fetching); this client-side call uses the public token, and its components/enableSync populate the client module graph the fallback reads from. Use this only when you specifically need live editing on a statically-exported build — for default/standalone, prefer the /rsc LocalessDocument above, which needs no client-side registration at all.
Manual Integration
If you manage content state yourself without useLocaless or LocalessDocument, use localessSyncOn / localessSyncOnChange. They wrap the isSyncEnabled() check and the sync-ready wait internally, so you don't need to guard for browser/iframe context or race the sync script load:
'use client';
import { useEffect, useState } from "react";
import { LocalessComponent, localessEditable, localessSyncOnChange } from "@localess/react";
import type { Content, Page } from "./.localess/localess";
export function PageClient({ initialContent }: { initialContent: Content<Page> }) {
const [pageData, setPageData] = useState(initialContent.data);
useEffect(() => {
// No-op automatically if sync isn't enabled/usable — no manual guards needed.
localessSyncOnChange((event) => setPageData(event.data));
// No cleanup needed: window.localess has no .off() method
}, []);
return (
<main {...localessEditable(pageData)}>
{pageData?.body.map(item => (
<LocalessComponent key={item._id} data={item} links={initialContent.links} references={initialContent.references} />
))}
</main>
);
}localessSyncOnChange(callback) is shorthand for localessSyncOn(['input', 'change'], callback). Use localessSyncOn directly to subscribe to other event types:
import { localessSyncOn } from "@localess/react";
localessSyncOn(['save', 'publish'], (event) => console.info(`Content ${event.type}d`));Available events:
| Event | When |
|---|---|
input | User is typing in a field (real-time preview) |
change | Field value confirmed |
save | Content saved |
publish | Content published |
unpublish | Content unpublished |
pong | Editor heartbeat response |
enterSchema | Editor cursor enters a schema block |
hoverSchema | Editor cursor hovers over a schema block |
leaveSchema | Editor cursor leaves a schema block |
window.localessonly exposes.on()and.onChange()— there is no.off()method. PreferlocalessSyncOn/localessSyncOnChangeover callingwindow.localessdirectly — they handle the enabled/ready checks for you.