Skip to content

Getting started

Install and first live preview

By the end of this page you will have a page that takes a string of TSX and renders it as a live React component.

Step 1: Install#

Terminal
npm install next-live

This guide uses the built-in <LiveEditor>, which needs one optional peer dependency. Package managers do not install it for you:

Terminal
npm install prism-react-renderer

Step 2: Create the runner#

next-live compiles in the browser, so the component that uses it must be a Client Component.

app/apps/Runner.tsx
'use client';
import { LiveProvider, LivePreview, LiveError } from 'next-live';
import { LiveEditor } from 'next-live/editor';
export function Runner({ source }: { source: string }) {
return (
<LiveProvider code={source}>
<LiveEditor />
<LivePreview />
<LiveError />
</LiveProvider>
);
}

That is already a working playground. react is registered for you, so snippets can use hooks immediately.

Step 3: Your first preview#

Try the counter below. This is the same snippet you would pass as code={source}:

Preview

Source (editable)

Tab inserts spaces. Press Escape and then Tab to move focus out of the editor.

Render it from a Server Component page:

app/apps/page.tsx
import { Runner } from './Runner';
const EXAMPLE = `import { useState } from 'react';
export default function App() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>clicked {n} times</button>;
}`;
export default function AppsPage() {
return <Runner source={EXAMPLE} />;
}

Step 4: Give snippets access to your app#

By default a snippet can only import react. It cannot reach your store, UI kit, or npm packages on its own. You declare what it may import through the modules prop: a map from import name to real code in your app.

Example: register '@app/store' so snippets can write import { useCart } from '@app/store' and get your actual store module.

tsx
'use client';
import { LiveProvider, LivePreview, LiveError, defineLoader } from 'next-live';
export function Runner({ source, user }) {
return (
<LiveProvider
code={source}
modules={{
'@app/store': defineLoader(() => import('@/lib/store')),
'@app/ui': defineLoader(() => import('@/components/ui')),
}}
props={{ user }}
>
<LivePreview />
<LiveError />
</LiveProvider>
);
}

Snippet authors then write ordinary code:

tsx
import { useCart } from '@app/store';
import { Button } from '@app/ui';
export default function App({ user }) {
const cart = useCart();
return <Button>{user.name}: {cart.length} items</Button>;
}

Read Module registry next for a full walkthrough, live demos, and common mistakes. It is the most important concept in next-live.

What each piece does#

ComponentPurpose
<LiveProvider>Compiles code and provides the result. Everything else must be inside it.
<LiveEditor>Textarea with syntax highlighting. Optional; omit for read-only runners.
<LivePreview>Renders the compiled component inside an error boundary.
<LiveError>Shows compile or runtime errors; renders nothing when healthy.

You can skip the components and use useLiveRunner for a fully custom UI.

What a snippet may look like#

All of these work:

Supported snippet shapes
export default function App() { return <div/> } // a module (recommended)
function App() { return <div/> } // bare declaration
<div>hello</div> // bare expression
() => <div/> // bare expression
render(<App prop="x"/>) // explicit render call
export default is the unambiguous form. The others are recovered heuristically for legacy snippets.

Prefer export default in anything you store; the heuristics exist for older authoring styles, not as a pattern to adopt.

TypeScript works too: types, interfaces, and generics are all stripped. Note that they are not checked; see Troubleshooting.

SSR and hydration#

Nothing is compiled during the server pass. <LivePreview> renders its fallback on the server and on the client's first render, so the two are identical and hydration cannot mismatch. Compilation starts afterwards, in an effect.

That is why you do not need next/dynamic(..., { ssr: false }). The reflex most people reach for first.

Give the fallback roughly the size of your content to avoid layout shift:

tsx
<LivePreview fallback={<div style={{ height: 320 }} />} />

Loading snippets from your API#

The real use case is code stored elsewhere. code is controlled: change it and the preview follows:

tsx
'use client';
import { useEffect, useState } from 'react';
import { LiveProvider, LivePreview, LiveError } from 'next-live';
export function RemoteApp({ id }) {
const [source, setSource] = useState(null);
useEffect(() => {
fetch(`/api/apps/${id}`)
.then((r) => r.json())
.then((data) => setSource(data.source));
}, [id]);
if (!source) return <p>Loading…</p>;
return (
<LiveProvider code={source}>
<LivePreview />
<LiveError />
</LiveProvider>
);
}

Before you deploy#

Read Security. It covers the CSP and the one rule that actually protects you.

Next steps#