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.

'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

LocalessDocument subscribes to input / change editor events automatically when enableSync is active.

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 (see the SPA example below).

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.

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
inputUser is typing in a field (real-time preview)
changeField value confirmed
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