better-content/react
The React binding. Ships with "use client" directives; React 18 or newer.
PageProvider
type PageProviderProps = { children: ReactNode } & (
| ({ engine: CmsEngine } & Partial<CmsEngineOptions>) // bind an external engine
| ({ engine?: undefined } & CmsEngineOptions) // or create one from options
);
<PageProvider transport={...} storage={...} notify={...} initialItems={...}>
<PageProvider engine={sharedEngine}>Creates one engine for its lifetime and provides both contexts, or binds an engine you created with createCmsEngine — the way to share one engine across multiple React roots or across frameworks (e.g. Astro islands). All props are captured at mount; changing them later does not recreate or swap the engine (remount with a key if you need to).
Hooks
function usePageContext(): PageContextValue;
// snapshot fields + every engine op + the engine itself; coarse, re-renders on any change
function useCmsEngine(): CmsEngine;
// the stable engine; never re-renders by itself
function useCmsItem<T = Record<string, unknown>>(collection: string, id: string): Item<T> | undefined;
// fine-grained: re-renders only when this item's reference changesAll three throw outside a PageProvider.
ContentEditSpan
interface ContentEditSpanProps {
collection: string;
itemId: string;
fieldKey: string; // dotted paths supported
as?: ElementType; // default "span"
className?: string;
children?: ReactNode; // string children act as fallback text
renderValue?: (raw: string) => ReactNode; // default: plain text
}Headless inline text editor. Reads edit mode from useCmsAuth. Commits drafts on blur via editField. Preserves multi-line input as \n and renders with white-space: pre-wrap. Styling hooks: data-cms-editable, data-cms-editing, data-cms-focused.
EditableImage
interface EditableImageProps {
collection: string;
itemId: string;
fieldKey: string;
src: string;
className?: string;
children?: (state: EditableImageRenderState) => ReactNode;
}
interface EditableImageRenderState {
src: string; // preview, external URL, or saved src
isEditing: boolean;
saving: boolean;
hasError: boolean; // current src failed to load
openFilePicker: () => void;
setExternalUrl: (url: string) => boolean; // false if not a valid http(s) URL
imgProps: { src: string; onError: () => void };
}Render-prop image editor over the pending-image queue. Renders a bare <img> without children.
useMarkdownEditor
function useMarkdownEditor(options: {
initialValue: string;
onSave: (content: string) => void | Promise<void>;
}): MarkdownEditorApi;
interface MarkdownEditorApi {
value: string;
setValue(next: string): void;
textareaRef: RefObject<HTMLTextAreaElement | null>;
insert(before: string, after?: string, placeholder?: string): void;
reset(to?: string): void; // defaults to the initial value
save(): void | Promise<void>; // passes the current value to onSave
charCount: number;
}insert wraps the textarea's current selection, or inserts placeholder when the selection is empty, and leaves the caret at the end of the wrapped text so typing continues inside the wrap. Without textareaRef attached it appends at the end of the value.
reset() returns to the value the editor was created with. A later change to the initialValue prop does not move that target, and does not reseed value either.
The Vue and Svelte bindings ship the same primitive as useMarkdownEditor and markdownEdit.
Auth context
function useCmsAuth(): CmsAuthState; // throws outside a provider
<CmsAuthProvider value={cmsAuthState}> // bring your own auth
<AnonymousEditProvider> // local edit toggle, isAdmin stays falseCmsAuthContext is exported for advanced composition; built-in auth providers feed the same context instance through the public specifier.