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#
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:
npm install prism-react-renderer
Preview-only pages should skip it
If you only run stored snippets and never edit them, you never import
next-live/editor, which is exactly why the highlighter lives on a separate
entry. Skipping it is what keeps a preview-only page at 16.1 KB.
Step 2: Create the runner#
next-live compiles in the browser, so the component that uses it must be a Client Component.
'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)
Render it from a Server Component page:
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} />;}
You do not need next/dynamic with ssr: false
next-live never compiles during the server pass, so the first client render matches the server exactly. How it works.
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.
'use client';import { LiveProvider, LivePreview, LiveError, defineLoader } from 'next-live';export function Runner({ source, user }) {return (<LiveProvidercode={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:
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#
| Component | Purpose |
|---|---|
<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:
export default function App() { return <div/> } // a module (recommended)function App() { return <div/> } // bare declaration<div>hello</div> // bare expression() => <div/> // bare expressionrender(<App prop="x"/>) // explicit render call
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:
<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:
'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>);}
Only fetch from your own authenticated API
Never load snippet source from a URL query param, hash fragment, or localStorage. See Security.
Before you deploy#
Read Security. It covers the CSP and the one rule that actually protects you.
