Every export is a named binding, never a property on a parent object.
Across the RSC boundary a Server Component receives a client reference, so
Live.Preview would resolve to undefined.
import { LiveProvider, LivePreview, defineLoader } from 'next-live';import { LiveEditor } from 'next-live/editor'; // optional peer: prism-react-rendererimport { precompile, validateSnippets } from 'next-live/server'; // server only
| Entry | Contains | Why it is separate |
|---|---|---|
next-live | Provider, preview, error, hooks, registry, engine | - |
next-live/editor | <LiveEditor> | The only thing needing prism-react-renderer, an optional peer you must install yourself. A preview-only page pays 16.1 KB instead of 97.2 KB. |
next-live/server | precompile, validateSnippet(s) | Imports Sucrase statically, must never reach the client bundle. |
Components#
function LiveProvider(props: LiveProviderProps): ReactNode
Compiles code and provides the result to its children. Everything else must be
inside it. Safe to render from a Server Component; nothing compiles during the
server pass.
| Prop | Type | Default | Notes |
|---|---|---|---|
| coderequired | string | - | The snippet. Controlled: change it and the preview follows. |
| modules | ModuleRegistry | {} | Specifier → value or loader. Merges over the built-ins. |
| scope | LiveScope | {} | Free variables injected as bare identifiers without import. |
| props | Record<string, unknown> | {} | Passed to the rendered component by reference. |
| fallback | ReactNode | null | Rendered until the first compile finishes. Size it like your content to avoid layout shift. |
| language | string | 'tsx' | Highlighting hint for <LiveEditor>. |
| onError | (error: Error) => void | - | Called on every compile and runtime error. |
| onCodeChange | (code: string) => void | - | Called when the code is edited from inside. Not called when the code prop changes from outside, so it cannot echo your own saves back. This is what a control panel needs to persist edits. |
| onCompileSuccess | (info: CompileSuccessInfo) => void | - | After every successful compile, with compileId, sorted imports, via, and durationMs. Not called on failure or abort. |
| debounce | number | 150 | Milliseconds before recompiling after a change. |
| keepLastGood | boolean | true | Keep the last working component mounted when a recompile fails. |
| maxRendersPerSecond | number | 1000 | Render-loop breaker threshold. |
| transform | TransformFn | - | Replace the built-in Sucrase pass, e.g. server-precompiled output. |
| resolveSubpaths | boolean | false | Resolve pkg/Sub against a registered pkg by property access. |
| filePath | string | 'LiveCode.tsx' | Name shown in stack traces and DevTools. |
| production | boolean | true | false selects react/jsx-dev-runtime for richer component stacks. |
| jsxRuntime | 'automatic' | 'classic' | 'automatic' | - |
| jsxImportSource | string | 'react' | Register <source>/jsx-runtime if you change this. |
coderequired
- Type
string- Default
- -
The snippet. Controlled: change it and the preview follows.
modules
- Type
ModuleRegistry- Default
- {}
Specifier → value or loader. Merges over the built-ins.
scope
- Type
LiveScope- Default
- {}
Free variables injected as bare identifiers without import.
props
- Type
Record<string, unknown>- Default
- {}
Passed to the rendered component by reference.
fallback
- Type
ReactNode- Default
- null
Rendered until the first compile finishes. Size it like your content to avoid layout shift.
language
- Type
string- Default
- 'tsx'
Highlighting hint for <LiveEditor>.
onError
- Type
(error: Error) => void- Default
- -
Called on every compile and runtime error.
onCodeChange
- Type
(code: string) => void- Default
- -
Called when the code is edited from inside. Not called when the code prop changes from outside, so it cannot echo your own saves back. This is what a control panel needs to persist edits.
onCompileSuccess
- Type
(info: CompileSuccessInfo) => void- Default
- -
After every successful compile, with compileId, sorted imports, via, and durationMs. Not called on failure or abort.
debounce
- Type
number- Default
- 150
Milliseconds before recompiling after a change.
keepLastGood
- Type
boolean- Default
- true
Keep the last working component mounted when a recompile fails.
maxRendersPerSecond
- Type
number- Default
- 1000
Render-loop breaker threshold.
transform
- Type
TransformFn- Default
- -
Replace the built-in Sucrase pass, e.g. server-precompiled output.
resolveSubpaths
- Type
boolean- Default
- false
Resolve pkg/Sub against a registered pkg by property access.
filePath
- Type
string- Default
- 'LiveCode.tsx'
Name shown in stack traces and DevTools.
production
- Type
boolean- Default
- true
false selects react/jsx-dev-runtime for richer component stacks.
jsxRuntime
- Type
'automatic' | 'classic'- Default
- 'automatic'
jsxImportSource
- Type
string- Default
- 'react'
Register <source>/jsx-runtime if you change this.
Preview
Source sent to LiveProvider
Anything in props is handed to the rendered component by reference, this is
how a snippet reads host state it must not be able to import:
Preview
Source sent to LiveProvider
function LivePreview(props: LivePreviewProps): ReactNode
Renders the compiled component inside an error boundary.
| Prop | Type | Default | Notes |
|---|---|---|---|
| props | Record<string, unknown> | - | Merged over the provider's props. |
| fallback | ReactNode | provider's | Shown until the first compile finishes. |
| as | ElementType | 'div' | Wrapper element. |
| className / style | - | - | Applied to the wrapper. |
props
- Type
Record<string, unknown>- Default
- -
Merged over the provider's props.
fallback
- Type
ReactNode- Default
- provider's
Shown until the first compile finishes.
as
- Type
ElementType- Default
- 'div'
Wrapper element.
className / style
- Type
-- Default
- -
Applied to the wrapper.
function LiveEditor(props: LiveEditorProps): ReactNode
A <textarea> layered over syntax-highlighted output. No editor engine, so it
stays small and has no SSR quirks. On its own entry point because it is the only
thing that pulls in prism-react-renderer.
| Prop | Type | Default | Notes |
|---|---|---|---|
| renderEditor | (props: LiveEditorRenderProps) => ReactNode | - | Replace the built-in editor entirely, drop in CodeMirror or Monaco. |
| theme | PrismTheme | themes.vsDark | From prism-react-renderer. |
| prism | typeof Prism | built-in | A Prism instance with extra languages registered, for anything outside the built-in set. |
| readOnly | boolean | inferred | Display source without editing. Defaults to true when there is nothing to write edits to. |
| tabSize | number | 2 | Spaces inserted by the Tab key. |
| padding | number | 16 | - |
| errorLineStyle | CSSProperties | null | red inset highlight | Paint-only highlight on the error line. Pass null to disable. |
| errorLineClassName | string | - | Extra class on the error line. |
| focusRingStyle | CSSProperties | null | 2px blue outline | Ring painted while the editor holds keyboard focus. Pass null only if you paint your own. |
| aria-label | string | 'Live code editor' | Accessible name. |
| code | string | from context | Standalone mode, see below. |
| onChange | (code: string) => void | from context | Standalone mode, see below. |
| language | string | from context, then 'tsx' | - |
| error | Error | null | from context | Error to underline. |
renderEditor
- Type
(props: LiveEditorRenderProps) => ReactNode- Default
- -
Replace the built-in editor entirely, drop in CodeMirror or Monaco.
theme
- Type
PrismTheme- Default
- themes.vsDark
From prism-react-renderer.
prism
- Type
typeof Prism- Default
- built-in
A Prism instance with extra languages registered, for anything outside the built-in set.
readOnly
- Type
boolean- Default
- inferred
Display source without editing. Defaults to true when there is nothing to write edits to.
tabSize
- Type
number- Default
- 2
Spaces inserted by the Tab key.
padding
- Type
number- Default
- 16
errorLineStyle
- Type
CSSProperties | null- Default
- red inset highlight
Paint-only highlight on the error line. Pass null to disable.
errorLineClassName
- Type
string- Default
- -
Extra class on the error line.
focusRingStyle
- Type
CSSProperties | null- Default
- 2px blue outline
Ring painted while the editor holds keyboard focus. Pass null only if you paint your own.
aria-label
- Type
string- Default
- 'Live code editor'
Accessible name.
code
- Type
string- Default
- from context
Standalone mode, see below.
onChange
- Type
(code: string) => void- Default
- from context
Standalone mode, see below.
language
- Type
string- Default
- from context, then 'tsx'
error
- Type
Error | null- Default
- from context
Error to underline.
Standalone, without a provider#
<LiveEditor> normally takes its code from the surrounding <LiveProvider> and
sends edits back to it. Pass code and it works on its own, which is how a page
shows a highlighted snippet it is not running:
// Read-only: no onChange, so there is nowhere to write an edit.<LiveEditor code={source} language="tsx" />// Editable, driven by your own state.<LiveEditor code={code} onChange={setCode} />
With neither a provider nor code, the editor throws rather than rendering
empty.
Keyboard access
Tab inserts spaces, because an editor that moves focus on Tab cannot be typed
into. Press Escape, then Tab, to move focus out, the same convention
CodeMirror and Monaco use. The editor announces this through aria-keyshortcuts
and a visually-hidden description, and paints a focus ring while focused, so it
satisfies WCAG 2.1.2 (No Keyboard Trap) and 2.4.7 (Focus Visible). Any other
keystroke re-arms indentation, so Escape only ever releases the very next Tab.
LiveEditorRenderProps exposes code, onChange, language, error,
errorLine, and errorColumn, everything a third-party editor needs:
<LiveEditorrenderEditor={({ code, onChange, language, errorLine }) => (<CodeMirror value={code} onChange={onChange} lang={language} highlightLine={errorLine} />)}/>
Preview
Source (editable)
function LiveError(props: LiveErrorProps): ReactNode
Shows the current compile or runtime error; renders nothing when healthy. Both phases surface here, so it is a single place to look.
| Prop | Type | Default | Notes |
|---|---|---|---|
| children | (error: Error) => ReactNode | built-in rendering | Custom error rendering. |
| as | ElementType | 'pre' | - |
| className / style | - | - | - |
children
- Type
(error: Error) => ReactNode- Default
- built-in rendering
Custom error rendering.
as
- Type
ElementType- Default
- 'pre'
className / style
- Type
-- Default
- -
function LiveErrorBoundary(props: LiveErrorBoundaryProps): ReactNode
Used internally by <LivePreview>. Exported for custom UIs that render the
compiled component themselves.
| Prop | Type | Default | Notes |
|---|---|---|---|
| onErrorrequired | (error: Error) => void | - | Called when rendering the snippet throws. |
| resetKey | unknown | - | Changing it clears the error. Wire this to compileId. |
| fallback | ReactNode | - | Rendered while in the error state. |
onErrorrequired
- Type
(error: Error) => void- Default
- -
Called when rendering the snippet throws.
resetKey
- Type
unknown- Default
- -
Changing it clears the error. Wire this to compileId.
fallback
- Type
ReactNode- Default
- -
Rendered while in the error state.
Hooks#
Snippets share your application's single React instance, so hooks, effects, and
context all work across the boundary. The timer below is running useState and
useEffect from your React:
Preview
Source sent to LiveProvider
function useLiveRunner(options: UseLiveRunnerOptions): LiveRunnerState
The headless engine behind <LiveProvider>, for building a completely custom
UI. Accepts every LiveProvider option except props, language, onError,
and fallback.
const { code, setCode, Component, element, error, isCompiling, compileId } =useLiveRunner({ code: source, modules });
| Field | Type | Default | Notes |
|---|---|---|---|
| code | string | - | Current source. |
| setCode | (code: string) => void | - | Stable identity. |
| Component | ComponentType | null | - | null until the first successful compile, including during SSR. |
| element | ReactElement | null | - | Set instead of Component when the snippet produced an element. |
| error | Error | null | - | - |
| isCompiling | boolean | - | Only true after ~200 ms, so fast compiles never flash a spinner. |
| compileId | number | - | Increments on every successful compile. Use as a remount key. |
code
- Type
string- Default
- -
Current source.
setCode
- Type
(code: string) => void- Default
- -
Stable identity.
Component
- Type
ComponentType | null- Default
- -
null until the first successful compile, including during SSR.
element
- Type
ReactElement | null- Default
- -
Set instead of Component when the snippet produced an element.
error
- Type
Error | null- Default
- -
isCompiling
- Type
boolean- Default
- -
Only true after ~200 ms, so fast compiles never flash a spinner.
compileId
- Type
number- Default
- -
Increments on every successful compile. Use as a remount key.
function useLiveModule<T>(options: UseLiveModuleOptions): LiveModuleState<T>
Runs a snippet and returns its exports, for stored code that is not a component
- a validator, a transformer, a calculated field. Accepts every
useLiveRunneroption exceptmaxRendersPerSecond(nothing renders, so there is no render loop to break).
| Field | Type | Default | Notes |
|---|---|---|---|
| exports | T | null | - | Everything the snippet exported. null until the first successful run. |
| value | unknown | - | Shorthand for exports?.default. |
| error | Error | null | - | - |
| isCompiling | boolean | - | - |
| compileId | number | - | - |
exports
- Type
T | null- Default
- -
Everything the snippet exported. null until the first successful run.
value
- Type
unknown- Default
- -
Shorthand for exports?.default.
error
- Type
Error | null- Default
- -
isCompiling
- Type
boolean- Default
- -
compileId
- Type
number- Default
- -
The type parameter is a claim, not a check
Writing useLiveModule<PricingRule>() does not verify that the snippet
actually exports a PricingRule. Validate the shape at runtime before
trusting it.
useLiveModule: non-UI snippet
function useLiveContext(): LiveContextValue
Reads the surrounding <LiveProvider>, for custom editors, toolbars, or status
indicators. Throws if called outside a provider. Returns every
useLiveRunner field plus props, language, fallback, and
reportRuntimeError.
'use client';import { useLiveContext } from 'next-live';export function CompileStatus() {const { isCompiling, error, compileId } = useLiveContext();if (isCompiling) return <span>Compiling…</span>;if (error) return <span className="text-destructive">Failed</span>;return <span>Compiled #{compileId}</span>;}
Registry helpers#
function defineLoader(load: ModuleLoader): ModuleLoader
Marks a function as a lazy loader rather than as the module value itself. Without it, a registered component function would be indistinguishable from a loader. This is the default way to register a module; it keeps your own dependencies out of the page bundle.
The loader receives the imported specifier, which is what prefix entries need.
// Exact match, the specifier can be ignored.'@app/store': defineLoader(() => import('@/lib/store')),// Prefix entry (key ends in '/'), one loader serves the whole subtree.'big-lib/': defineLoader((specifier) => import(`big-lib/${specifier.slice(8)}`)),
function defineModule(shape: { default?: unknown; exports?: Record<string, unknown> }): NormalizedModuleBuilds an explicit module record. A registered object with its own default key
is normally unwrapped; use this when the whole object is the default export.
default and exports.default address the same slot.
function createRegistry(...groups: ModuleRegistry[]): ModuleRegistry
Merges registry groups, later groups winning. Warns in development when two groups define the same key, which is how you catch a silent override.
function registryFromGlob(glob: GlobResult, toSpecifier: (path: string) => string | null): ModuleRegistry
Converts import.meta.glob's lazy result into a registry of loaders.
toSpecifier maps a file path to the specifier authors write; return null to
omit a file.
const builtinModules: ModuleRegistry
The always-registered map: react, react/jsx-runtime, and
react/jsx-dev-runtime. Your modules merge over these, so a React shim can
still be substituted. react-dom is deliberately absent, register it
explicitly if you need it.
Experimental APIs#
These exports are marked @experimental in source. They may change in minor
releases without a major bump. Prefer the stable engine surface below for
production integrations.
| Export | Purpose |
|---|---|
setTranspiler(module) | Swap the transpiler implementation. Intended for tests and custom backends. |
createRenderBudget(options) | Configure the render-loop breaker used during evaluation. |
Engine#
Lower-level exports, for a custom scheduler or for compiling outside React.
function compile(input: CompileInput): Promise<CompileResult>
Transpile, resolve every import, then evaluate. Returns { renderable, via, code, imports }. Throws if the snippet produced nothing renderable.
function compileModule(input: CompileInput): Promise<CompileModuleResult>
The same pipeline, returning { exports, code, imports } with no component
required.
function transpile(source: string, options: TranspileOptions, transform?: TransformFn): Promise<TransformResult>
Source → CommonJS. No evaluation, no module resolution.
function preloadTranspiler(): Promise<void>
Warms the lazily-fetched Sucrase chunk during idle time, so the first compile does not pay for the download. Call it when you know an editor is about to open.
function precompiledTransform(result: TransformResult): TransformFn
Wraps a server-precompiled result as a transform, so the client never loads
Sucrase at all. Exported from the client entry, importing it from
next-live/server would pull the transpiler into your page.
function setTranspiler(module: unknown): void
Swaps the transpiler outright. For tests and custom backends.
function errorPosition(error: Error | null | undefined): { line: number; column?: number } | nullReads { line, column? } off a compile or runtime error, and returns null
when there is no position. This is what a custom editor uses to paint the
failing line. Pass it the error from useLiveRunner or useLiveContext
directly; a null error is handled.
function createRenderBudget(options?: RenderBudgetOptions): () => void
The render-loop breaker. A fresh budget is created per compile, so fixing a runaway snippet clears a tripped breaker without a page reload.
function normalizeModule(value: ModuleValue): NormalizedModule
The interop normalisation applied to every registry value: maps an arbitrary
object, function, or namespace onto default plus named exports.
function createRequire(resolved: ResolvedModules): (specifier: string) => NormalizedModule
The synchronous require shim handed to compiled snippets. It must stay
synchronous: Sucrase emits require() at module top level, which cannot await.
function resolveModules(options: ResolveOptions): Promise<ResolvedModules>
Resolves every specifier a snippet needs before evaluation, which is why
async loaders work at all despite require() being synchronous.
Server entry#
Never import this from a Client Component
next-live/server imports Sucrase statically. It carries no 'use client'
directive and pulls in no React, so it is safe in Route Handlers and Server
Components, and only there.
function precompile(source: string, options?: TranspileOptions): PrecompileResult
Transpiles to exactly the same CommonJS the browser path produces, so a precompiled result and a client-compiled one are interchangeable. Transpile once, cache by hash, and the client never downloads the transpiler.
| Field | Type | Default | Notes |
|---|---|---|---|
| code | string | - | The transpiled CommonJS. |
| hash | string | - | Stable hash of the source and the options that affect output. Use it as a cache key or ETag. |
| linePrefixOffset | number | - | Lines the wrapper added above the snippet; needed to map error lines back. |
| expression | boolean | - | Whether the snippet compiled as a bare expression rather than a module. |
code
- Type
string- Default
- -
The transpiled CommonJS.
hash
- Type
string- Default
- -
Stable hash of the source and the options that affect output. Use it as a cache key or ETag.
linePrefixOffset
- Type
number- Default
- -
Lines the wrapper added above the snippet; needed to map error lines back.
expression
- Type
boolean- Default
- -
Whether the snippet compiled as a bare expression rather than a module.
PrecompileResult extends TransformResult, so a result hands straight to
precompiledTransform.
function validateSnippet(source: string, options?: ValidateOptions): ValidationResult
Statically checks that a snippet compiles and that every import resolves.
Never evaluates, so it is safe to run over untrusted content in CI.
modules accepts a registry object or just its keys.
const result = validateSnippet(source, { modules: ['@app/store', 'big-lib/'] });// { ok, issues: [{ kind, message, specifier?, suggestion?, line?, column? }], imports }
| Option | Type | Default | Notes |
|---|---|---|---|
| maxSourceBytes | number | - | Reject snippets over this UTF-8 byte count before transpile. |
| forbidNodeBuiltins | boolean | - | Treat node:* imports as forbidden. |
| forbidRemoteImports | boolean | - | Treat https://, http://, and // imports as forbidden. |
| denySpecifiers | readonly string[] | - | Deny listed specifiers even when registered. A trailing / denies a subtree. |
maxSourceBytes
- Type
number- Default
- -
Reject snippets over this UTF-8 byte count before transpile.
forbidNodeBuiltins
- Type
boolean- Default
- -
Treat node:* imports as forbidden.
forbidRemoteImports
- Type
boolean- Default
- -
Treat https://, http://, and // imports as forbidden.
denySpecifiers
- Type
readonly string[]- Default
- -
Deny listed specifiers even when registered. A trailing / denies a subtree.
All policy flags are opt-in; defaults are unchanged.
function validateSnippets(snippets, options?): { id: string; result: ValidationResult }[]Validates many at once and returns only the failures, so a CI step can
assert on an empty array. Each snippet is { id, source }.
Errors#
All extend LiveError, exported as LiveErrorBase so it does not collide with
the <LiveError> component.
| Class | Raised when |
|---|---|
LiveCompileError | Parse/transpile failure, or a CSP blocking eval. Carries line and column. |
LiveRuntimeError | The snippet threw. Carries line where it can be mapped. |
RenderLoopError | The render-rate breaker tripped. |
ModuleNotFoundError | An import specifier is not registered. Carries specifier and available. |
NoComponentError | The snippet produced nothing renderable. |
TranspilerLoadError | Sucrase failed to load, usually a chunk-load failure. |
A snippet that throws while rendering is caught by the boundary inside
<LivePreview> and surfaced through <LiveError>; the page itself never goes
down:
Types#
ModuleRegistry, ModuleLoader, ModuleValue, NormalizedModule, LiveScope,
LiveRenderable, LiveRunnerState, LiveContextValue, CompileOptions,
CompileResult, CompileInput, CompileModuleResult, CompileSuccessInfo,
TranspileOptions, TransformFn, TransformResult, ExtractionSource,
UseLiveRunnerOptions, UseLiveModuleOptions, LiveModuleState,
RenderBudgetOptions, GlobResult, PositionedError, PrecompileResult,
ValidationResult, ValidationIssue, ValidationIssueKind (syntax,
unresolved-import, source-too-large, forbidden-import), ValidateOptions,
plus a props type for every component, LiveProviderProps, LivePreviewProps,
LiveEditorProps, LiveEditorRenderProps, LiveErrorProps,
LiveErrorBoundaryProps.
