Localess
Next.js

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, useLocaless runs in the browser, where the server-side localessInit() never ran — call localessInit a 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:

PropTypeRequiredDescription
documentContent<T>YesFull content response object (from getContentBySlug/getContentById)
refReact.Ref<HTMLElement>NoForwarded to the rendered root element

When enableSync is active, LocalessDocument applies input and change edits live, and clears them on save, publish and unpublish so 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/react export's LocalessDocument is 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's localessInit() call was never applied, and it fails to find any registered component. Use the plain @localess/react export's LocalessDocument only from inside an actual 'use client' file.

Known limitation: the /rsc LocalessDocument's live-edit cache is in-process memory. On a default-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 affect standalone deployments or local development. Requires a live server at request time — does not work under output: 'export'; use the client-side fallback below instead.

See the playgrounds/next playground 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 /rsc under the alias LocalessClientDocument. That alias no longer exists — import LocalessDocument from @localess/react directly.

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:

EventWhen
inputA field value changed — every keystroke (real-time preview)
changeBlocks added, removed, duplicated or reordered — and once when the preview connects, with the current content
saveContent saved
publishContent published
unpublishContent unpublished
pongEditor heartbeat response
enterSchemaEditor cursor enters a schema block
hoverSchemaEditor cursor hovers over a schema block
leaveSchemaEditor cursor leaves a schema block

window.localess only exposes .on() and .onChange() — there is no .off() method. Prefer localessSyncOn/localessSyncOnChange over calling window.localess directly — they handle the enabled/ready checks for you.

On this page