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.
In Next.js,
useLocalessruns in the browser, where the server-sidelocalessInit()never ran — calllocalessInita second time inside the Client Component boundary with a public token, as in the static export fallback below.
'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 |
When
enableSyncis active,LocalessDocumentappliesinputandchangeedits live — only those for its own document, matched bydocument.id, so other documents on the page are unaffected — and clears them onsave,publishandunpublishso the page falls back to the API.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.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. It applies only its own document's edits (matched by document.id), and replays the latest edit if it mounts after the editor sent it.
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 localessSyncOnDocument. It subscribes to the input and change edits of one document, matched by its id, so edits to other documents on the page are ignored, and it replays the latest edit if it subscribes late. It wraps 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, and it returns an unsubscribe function — return it from useEffect:
'use client';
import { useEffect, useState } from "react";
import { LocalessComponent, localessEditable, localessSyncOnDocument } from "@localess/react";
import type { Content, Page } from "./.localess/localess";
export function PageClient({ initialContent }: { initialContent: Content<Page> }) {
const [pageData, setPageData] = useState(initialContent.data);
// No-op automatically if sync isn't enabled/usable — no manual guards needed.
useEffect(
() => localessSyncOnDocument<Page>(initialContent.id, (data) => setPageData(data)),
[initialContent.id],
);
return (
<main {...localessEditable(pageData)}>
{pageData?.body.map(item => (
<LocalessComponent key={item._id} data={item} links={initialContent.links} references={initialContent.references} />
))}
</main>
);
}For other event types, or for edits to every document on the page, use localessSyncOn — or localessSyncOnChange(callback), shorthand for localessSyncOn(['input', 'change'], callback). Both return an unsubscribe function too:
import { useEffect } from "react";
import { localessSyncOn } from "@localess/react";
useEffect(() => localessSyncOn(['save', 'publish'], (event) => console.info(`Content ${event.type}d`)), []);Available events:
| Event | When |
|---|---|
input | A field value changed — every keystroke (real-time preview) |
change | Blocks added, removed, duplicated or reordered — and once when the preview connects, with the current content |
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 |
Every input, change, save, publish and unpublish event carries a documentId — the id of the edited document, as returned by the API — so a page rendering several documents can apply each edit to the right one. LocalessDocument (<ll-document> in Angular) filters on it for you; check event.documentId yourself only when you subscribe to raw events. Because edits are matched by documentId, update your Localess deployment together with the SDK — see Troubleshooting.
window.localessexposes.on(),.onChange()and.off(), and.on()/.onChange()return an unsubscribe function. Prefer the SDK helpers (localessSyncOnDocument,localessSyncOn,localessSyncOnChange) over callingwindow.localessdirectly — they also handle the enabled/ready checks for you.