Visual Editor
Live-editing patterns for React Router — 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.
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 renders a full content response and applies live edits from the Visual Editor itself. Fetch the document in the route's loader, then hand it over:
// app/routes/catch-all.tsx
import { getLocalessClient, LocalessDocument } from "@localess/react";
import type { Page } from "~/shared/models/localess";
import type { Route } from "./+types/catch-all";
export async function loader({ params }: Route.LoaderArgs) {
const document = await getLocalessClient().getContentBySlug<Page>(params["*"] || "home");
return { document };
}
export default function CatchAllPage({ loaderData }: Route.ComponentProps) {
return <LocalessDocument document={loaderData.document} />;
}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. It applies only its own document's edits (matched bydocument.id), so other documents on the page are unaffected, and replays the latest edit if it mounts after the editor sent it.See the
playgrounds/react-routerplayground for a complete project, including locale resolution and 404 handling.
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:
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.