Localess
TanStack Start

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:

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

LocalessDocument subscribes to input / change editor events automatically when enableSync is active. It applies only its own document's edits (matched by document.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/tanstack-start playground 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:

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

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.localess exposes .on(), .onChange() and .off(), and .on()/.onChange() return an unsubscribe function. Prefer the SDK helpers (localessSyncOnDocument, localessSyncOn, localessSyncOnChange) over calling window.localess directly — they also handle the enabled/ready checks for you.

On this page