React
Package: @localess/react — v3.0.0
Package: @localess/react — v3.0.0
The @localess/react package is the official React integration for the Localess headless CMS platform. It provides component mapping, rich text rendering, and Visual Editor synchronization support for React applications.
⚠️ Security Notice: This package uses
@localess/clientinternally, which requires an API token for server-side data fetching. Always fetch Localess content server-side (e.g., Next.js Server Components, API routes, orgetServerSideProps) and never expose your token in client-side code.
Requirements
- Node.js >= 24.0.0
- React 17, 18, or 19
Installation
# npm
npm install @localess/react
# yarn
yarn add @localess/react
# pnpm
pnpm add @localess/reactChoosing the Right Export
@localess/react provides three different exports to suit different rendering strategies:
| Export | Use Case | Live Editing | Static Export |
|---|---|---|---|
@localess/react | Single Page Applications (SPA), client-side rendering | Yes | Yes |
@localess/react/ssr | SSR without live editing, Next.js static exports | No | Yes |
@localess/react/rsc | React Server Components with live editing | Yes | No |
When to Use Each Export
Use @localess/react (default) for:
- Single Page Applications (SPA) or fully client-rendered React apps
- Apps where
localessInitand components run entirely in the browser
Use @localess/react/ssr for:
- Next.js projects with
output: 'export'(static site generation) - Server-side rendering where live editing is not required
- Scenarios where bundle size matters and you want to exclude all browser-only sync code
Use @localess/react/rsc for:
- Next.js App Router with React Server Components
- Apps that need live Visual Editor editing alongside server rendering
- Modern Next.js apps with a server/client component split
Quick Comparison
// SPA — everything runs client-side
import { localessInit, LocalessComponent, useLocaless } from "@localess/react";
// SSR — server-safe, no live editing, no hooks
import { localessInit, LocalessServerComponent } from "@localess/react/ssr";
// RSC — server components + client components for live editing
import { localessInit, LocalessServerComponent } from "@localess/react/rsc"; // server
import { LocalessDocument, useLocaless, localessEditable } from "@localess/react/rsc"; // clientNote: When using Next.js with
output: 'export', always use@localess/react/ssr. The RSC export is not compatible with static exports.
Playgrounds: Full working Next.js projects are available for both modes —
playgrounds/next(App Router + RSC, live editing) andplaygrounds/next-static(output: 'export', static).
Getting Started
1. Initialize the SDK
Call localessInit once at application startup (e.g., in your root layout or _app.tsx) to configure the client, register your components, and optionally enable the Visual Editor.
import { localessInit } from "@localess/react";
import { Page, Header, Teaser, Footer } from "@/components";
localessInit({
origin: "https://my-localess.web.app",
spaceId: "YOUR_SPACE_ID",
token: "YOUR_API_TOKEN",
enableSync: true, // Enable Visual Editor sync script
components: {
'page': Page,
'header': Header,
'teaser': Teaser,
'footer': Footer,
},
});Initialization Options
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
origin | string | Yes | — | Fully qualified domain with protocol |
spaceId | string | Yes | — | Localess Space ID, found in Space settings |
token | string | Yes | — | Localess API token (keep secret — server-side only) |
version | 'draft' | string | No | 'published' | Default content version |
debug | boolean | No | false | Enable debug logging |
cacheTTL | number | false | No | 300 | Cache TTL in seconds. Set false to disable caching entirely |
components | Record<string, React.ElementType> | No | {} | Map of schema keys to React components |
fallbackComponent | React.ElementType | No | — | Component rendered when a schema key has no registered component |
enableSync | boolean | No | false | Load the Visual Editor sync script for live-editing support |
LocalessComponent
LocalessComponent is a dynamic renderer that maps Localess content data to your registered React components by schema key. It automatically applies Visual Editor attributes when sync is enabled.
import { LocalessComponent } from "@localess/react";
// Render a single content block
<LocalessComponent data={content.data} />
// Render a list of nested blocks
{data.body.map(item => (
<LocalessComponent
key={item._id}
data={item}
links={content.links}
references={content.references}
/>
))}Props
| Prop | Type | Required | Description |
|---|---|---|---|
data | ContentData | Yes | Content data object from Localess. The component looks up data._schema in the component registry |
links | Links | No | Resolved content links map, forwarded to the rendered component |
references | References | No | Resolved references map, forwarded to the rendered component |
assets | Assets | No | Resolved content assets map (keyed by asset ID), forwarded to the rendered component |
ref | React.Ref<HTMLElement> | No | Ref forwarded to the rendered component's root element |
...rest | any | No | Any additional props are forwarded to the rendered component |
If a schema key is not registered and no
fallbackComponentis configured,LocalessComponentrenders an error message in the DOM.
Marking Editable Content
Use these helpers to add Visual Editor attributes to your JSX elements. They enable element highlighting and selection in the Localess Visual Editor.
localessEditable(content)
Marks a content block root element as editable.
import { localessEditable } from "@localess/react";
const Header = ({ data }) => (
<nav {...localessEditable(data)}>
{/* ... */}
</nav>
);localessEditableField<T>(fieldName)
Marks a specific field within a content block as editable, with type-safe field name inference when combined with generated types.
import { localessEditableField } from "@localess/react";
const Hero = ({ data }: { data: HeroBlock }) => (
<section {...localessEditable(data)}>
<h1 {...localessEditableField<HeroBlock>('title')}>{data.title}</h1>
<p {...localessEditableField<HeroBlock>('subtitle')}>{data.subtitle}</p>
</section>
);Rich Text Rendering
renderRichTextToReact(content)
Converts a Localess ContentRichText object to a React node tree. Supports the full range of rich text formatting produced by the Localess editor.
import { renderRichTextToReact } from "@localess/react";
const Article = ({ data }) => (
<article>
<h1>{data.title}</h1>
<div>{renderRichTextToReact(data.body)}</div>
</article>
);Supported rich text elements:
- Document structure
- Headings (h1–h6)
- Paragraphs
- Text formatting: bold, italic, strikethrough, underline
- Ordered and unordered lists
- Code blocks (with syntax highlighting support)
- Links (inline)
Accessing the Client
getLocalessClient()
Returns the LocalessClient instance created during localessInit. Use this in server-side data-fetching functions.
import { getLocalessClient } from "@localess/react";
async function fetchPageData(locale?: string) {
const client = getLocalessClient();
return client.getContentBySlug<Page>('home', { locale });
}Throws an error if called before
localessInithas been executed.
Component Registry API
These functions allow dynamic management of the component registry after initialization.
import {
registerComponent,
unregisterComponent,
setComponents,
getComponent,
setFallbackComponent,
getFallbackComponent,
isSyncEnabled,
} from "@localess/react";
// Register a new component
registerComponent('hero-block', HeroBlock);
// Unregister a component
unregisterComponent('hero-block');
// Replace the entire registry
setComponents({ 'page': Page, 'hero': Hero });
// Retrieve a component by schema key
const Component = getComponent('hero');
// Configure the fallback component
setFallbackComponent(UnknownComponent);
// Get the current fallback component
const fallback = getFallbackComponent();
// Check if Visual Editor sync is enabled
const syncEnabled = isSyncEnabled();Assets
resolveAsset(asset, params?)
Resolves a ContentAsset object to a fully qualified URL using the initialized client's origin. Pass an AssetTransformParams object as the second argument to request a resized image or a different output format — the params are appended as query string parameters that the Localess asset endpoint uses to transform the image on the fly. See Image Transforms for the full parameter reference.
import { resolveAsset } from "@localess/react";
const Image = ({ data }) => (
<img src={resolveAsset(data.image)} alt={data.imageAlt} />
);import { resolveAsset } from "@localess/react";
function HeroImage({ image }: { image: ContentAsset }) {
return (
<img
// Smaller, WebP thumbnail for a card
src={resolveAsset(image, { w: 400, f: 'webp' })}
alt={image.alt ?? ''}
/>
);
}
function ProductImage({ image }: { image: ContentAsset }) {
return (
<img
// Fixed box crop + quality control
src={resolveAsset(image, { w: 800, h: 600, q: 70, f: 'avif' })}
alt={image.alt ?? ''}
/>
);
}| Param | Type | Description |
|---|---|---|
w | number | Target width in pixels |
h | number | Target height in pixels (combined with w, crops to cover the box) |
q | number | Output quality 1–100 (default 85; ignored for PNG) |
f | 'webp' | 'jpeg' | 'png' | 'avif' | Converts the output format |
download | boolean | Forces a browser download via Content-Disposition |
thumbnail | boolean | Extracts the first frame of an animated/video asset before resizing |
useLocaless Hook
useLocaless<T> fetches content by slug in a Client Component and automatically subscribes to Visual Editor live updates when enableSync is active.
'use client';
import { useLocaless, LocalessComponent } from "@localess/react";
import type { Page } from "./.localess/localess";
export function PageView({ slug }: { slug: string }) {
const content = useLocaless<Page>(slug, { locale: 'en' });
if (!content) return <div>Loading…</div>;
return (
<main>
{content.data.body.map(item => (
<LocalessComponent key={item._id} data={item} links={content.links} />
))}
</main>
);
}Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | string[] | Yes | Content slug. Arrays are joined with / — e.g. ['blog', 'post'] → 'blog/post' |
options | ContentFetchParams | No | Same fetch options as getContentBySlug (locale, version, resolveReference, resolveLink) |
Returns Content<T> | undefined — undefined while the initial fetch is in progress. Content<T> includes data, links, references, and assets (a map of resolved content assets keyed by asset ID).
When enableSync is active and the page is rendered inside the Localess Visual Editor iframe, the hook automatically subscribes to input / change events and updates the returned content in place.
Link Utilities
findLink(links, link)
Resolves a ContentLink field to a URL string. Use it to build href values from Localess content links.
import { findLink } from "@localess/react";
// type: 'content' → '/' + fullSlug, or '/not-found' if not in map
// type: 'url' → raw URI unchanged
const href = findLink(content.links, data.ctaLink);
const NavLink = ({ data, links }) => (
<a href={findLink(links, data.link)}>{data.label}</a>
);Visual Editor Events
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:
| 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.Import it from
@localess/react/rsc(as above) when calling it directly from a Server Component, as this example does — that variant renders the component registry lookup itself in the Server Component module graph, wherelocalessInit()'s registration is visible. The plain@localess/reactexport'sLocalessDocumentis 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'slocalessInit()call was never applied, and it fails to find any registered component. Use the plain@localess/reactexport'sLocalessDocumentonly from inside an actual'use client'file (see the SPA example below).See the
playgrounds/nextplayground 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:
| Event | When |
|---|---|
input | User is typing in a field (real-time preview) |
change | Field value confirmed |
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.
Full Example — SPA / Default (@localess/react)
For SPAs or fully client-rendered React apps. All imports use the default @localess/react export.
Setup — app/layout.tsx
// Server Component — safe to use API token here
import { localessInit } from "@localess/react";
import { Page, Header, Teaser, Footer } from "@/components";
localessInit({
origin: process.env.LOCALESS_ORIGIN!,
spaceId: process.env.LOCALESS_SPACE_ID!,
token: process.env.LOCALESS_TOKEN!,
enableSync: process.env.NODE_ENV !== 'production',
components: { Page, Header, Teaser, Footer },
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return <html><body>{children}</body></html>;
}Server Component — app/[locale]/page.tsx
import { getLocalessClient } from "@localess/react";
import type { Content, Page } from "./.localess/localess";
import { PageClientHook } from "./page-client-hook";
export default async function HomePage({
params,
}: {
params: Promise<{ locale?: string }>;
}) {
const { locale } = await params;
const content = await getLocalessClient().getContentBySlug<Page>('home', { locale });
return <PageClientHook initialContent={content} locale={locale} />;
}Client Component — Option A: useLocaless Hook
// app/[locale]/page-client-hook.tsx
'use client';
import { useLocaless, LocalessComponent, localessEditable } from "@localess/react";
import type { Content, Page } from "./.localess/localess";
export function PageClientHook({
initialContent,
locale,
}: {
initialContent: Content<Page>;
locale?: string;
}) {
const content = useLocaless<Page>('home', { locale }) ?? initialContent;
return (
<main {...localessEditable(content.data)}>
{content.data?.body.map(item => (
<LocalessComponent key={item._id} data={item} links={content.links} references={content.references} />
))}
</main>
);
}Client Component — Option B: LocalessDocument
This specific pattern — calling
LocalessDocumentdirectly from a Server Component with no separate'use client'file — needs the@localess/react/rscimport, not the plain@localess/reactexport used elsewhere in this SPA example. See WithLocalessDocumentComponent above for why.
// app/[locale]/page.tsx (Server Component — no separate client file needed)
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 content = await getLocalessClient().getContentBySlug<Page>('home', { locale });
// LocalessDocument handles sync internally — no 'use client' wrapper needed here
return <LocalessDocument document={content} />;
}Client Component — Option C: Manual
// app/[locale]/page-client-manual.tsx
'use client';
import { useEffect, useState } from "react";
import { LocalessComponent, localessEditable, isSyncEnabled, isBrowser } from "@localess/react";
import type { Content, Page } from "./.localess/localess";
export function PageClientManual({
initialContent,
}: {
initialContent: Content<Page>;
}) {
const [pageData, setPageData] = useState(initialContent.data);
useEffect(() => {
if (isSyncEnabled() && isBrowser() && window.localess) {
window.localess.on(['input', 'change'], (event) => {
if (event.type === 'input' || event.type === 'change') {
setPageData(event.data);
}
});
}
}, []);
return (
<main {...localessEditable(pageData)}>
{pageData?.body.map(item => (
<LocalessComponent key={item._id} data={item} links={initialContent.links} references={initialContent.references} />
))}
</main>
);
}Full Example — Next.js Static Export (@localess/react/ssr)
Use @localess/react/ssr when your Next.js project uses output: 'export' for static site generation. Live editing is not available in this mode.
next.config.js
/** @type {import('next').NextConfig} */
module.exports = { output: 'export' };Setup — lib/localess.ts
import { localessInit } from "@localess/react/ssr";
import { Page, Header, Teaser } from "@/components";
export const getClient = localessInit({
origin: process.env.LOCALESS_ORIGIN!,
spaceId: process.env.LOCALESS_SPACE_ID!,
token: process.env.LOCALESS_TOKEN!,
// enableSync is not applicable in static export — omit or set to false
components: { Page, Header, Teaser },
});Page — app/page.tsx
import { LocalessServerComponent } from "@localess/react/ssr";
import { getLocalessClient } from "@localess/react/ssr";
import "@/lib/localess"; // ensure init runs
export default async function Home() {
const client = getLocalessClient();
const content = await client.getContentBySlug("home", { locale: "en" });
return (
<main>
<LocalessServerComponent data={content.data} links={content.links} references={content.references} />
</main>
);
}@localess/react/ssr also exports LocalessServerDocument, which takes the full Content<T> response as a single document prop (like LocalessDocument, but with no sync attributes since live editing has no meaning once the HTML is pre-baked):
import { LocalessServerDocument } from "@localess/react/ssr";
<LocalessServerDocument document={content} />Playground:
playgrounds/next-staticmirrors the RSC playground but targetsoutput: 'export'and uses@localess/react/ssrend to end.
Full Example — Next.js App Router with RSC (@localess/react/rsc)
Use @localess/react/rsc when you want React Server Components and Visual Editor live editing together.
Setup — app/layout.tsx
// Server Component — safe to use API token here
import { localessInit } from "@localess/react/rsc";
import { Page, Header, Teaser, Footer } from "@/components";
localessInit({
origin: process.env.LOCALESS_ORIGIN!,
spaceId: process.env.LOCALESS_SPACE_ID!,
token: process.env.LOCALESS_TOKEN!,
enableSync: process.env.NODE_ENV !== 'production',
components: { Page, Header, Teaser, Footer },
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return <html><body>{children}</body></html>;
}Rendering — app/[locale]/page.tsx
Use LocalessDocument for a zero-boilerplate live-editing integration, or useLocaless for client-side re-fetching with more control.
Option A — LocalessDocument (recommended): it's a Server Component internally (only its sync subscription runs client-side), so it renders directly in the Server Component — no separate Client Component file needed.
import { getLocalessClient, LocalessDocument } from "@localess/react/rsc";
export default async function Home({ params }: { params: { locale: string } }) {
const { locale } = await params;
const content = await getLocalessClient().getContentBySlug("home", { locale });
return <LocalessDocument document={content} />;
}Option B — useLocaless hook: re-fetches on the client, so it needs an actual 'use client' file.
// app/[locale]/page.tsx (Server Component)
import { getLocalessClient } from "@localess/react/rsc";
import PageClient from "./page-client";
export default async function Home({ params }: { params: { locale: string } }) {
const { locale } = await params;
const content = await getLocalessClient().getContentBySlug("home", { locale });
return <PageClient initialContent={content} locale={locale} />;
}// app/[locale]/page-client.tsx (Client Component)
'use client';
import { useLocaless, LocalessComponent, localessEditable } from "@localess/react/rsc";
export default function PageClient({ initialContent, locale }) {
const content = useLocaless("home", { locale }) ?? initialContent;
return (
<main {...localessEditable(content.data)}>
{content.data?.body?.map(item => (
<LocalessComponent key={item._id} data={item} links={content.links} references={content.references} />
))}
</main>
);
}Playground:
playgrounds/nextis a full working Next.js App Router project built on Option A —localessInit()inpage.tsxand<LocalessDocument document={document} />rendered directly from the Server Component.
Export Reference
The table below shows which symbols are available in each export.
| Symbol | @localess/react | @localess/react/ssr | @localess/react/rsc |
|---|---|---|---|
localessInit | Yes | Yes | Yes |
getLocalessClient | Yes | Yes | Yes |
registerComponent / setComponents / getComponent | Yes | Yes | Yes |
setFallbackComponent / getFallbackComponent | Yes | Yes | Yes |
resolveAsset | Yes | Yes | Yes |
LocalessComponent | Yes | No | Yes |
LocalessServerComponent / LocalessServerDocument | No | Yes | Yes |
renderRichTextToReact | Yes | Yes | Yes |
findLink | Yes | Yes | Yes |
isServer | Yes | Yes | Yes |
| All content types | Yes | Yes | Yes |
LocalessDocument | Yes | No | Yes |
useLocaless | Yes | No | Yes |
localessEditable / localessEditableField | Yes | Yes | Yes |
isBrowser / isIframe | Yes | Yes | Yes |
isSyncEnabled / localessSyncOn / localessSyncOnChange / localessSyncReady | Yes | No | Yes |
Sync event types (LocalessSync, EventToApp, EventToAppOf, …) | Yes | Yes | Yes |
AI Coding Agents
This package ships a SKILL.md file that provides AI coding agents (GitHub Copilot, Claude Code, Cursor, and others) with accurate, up-to-date APIs, patterns, and best practices.
Reference it from your project's AGENTS.md:
## Localess
@node_modules/@localess/react/SKILL.md