Visual Editor
Live-editing patterns for TanStack Start — 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 a server function, call it from the route's loader, then hand it over:
// src/shared/server/get-page-content.ts
import { createServerFn } from "@tanstack/react-start";
import "virtual:localess-init";
import { getLocalessClient } from "@localess/react";
import type { Page } from "@/shared/models/localess";
export const getPageContent = createServerFn({ method: "GET" })
.validator((data: { slug: string }) => data)
.handler(({ data }) => getLocalessClient().getContentBySlug<Page>(data.slug));// src/routes/$.tsx
import { createFileRoute } from "@tanstack/react-router";
import { LocalessDocument } from "@localess/react";
import { getPageContent } from "@/shared/server/get-page-content";
export const Route = createFileRoute("/$")({
loader: async ({ params }) => ({
document: await getPageContent({ data: { slug: params._splat || "home" } }),
}),
component: CatchAllPage,
});
function CatchAllPage() {
const { document } = Route.useLoaderData();
return <LocalessDocument document={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.See the
playgrounds/tanstack-startplayground for a complete project, including locale resolution and 404 handling.
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:
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 | 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 |
window.localessonly exposes.on()and.onChange()— there is no.off()method. PreferlocalessSyncOn/localessSyncOnChangeover callingwindow.localessdirectly — they handle the enabled/ready checks for you.