next-live 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,337 @@
1
+ # API reference
2
+
3
+ [← Security](./05-security.md) · [Docs index](./README.md) · [Troubleshooting →](./07-troubleshooting.md)
4
+
5
+ Everything is exported as a **named** binding from `next-live`, never as a
6
+ property on a parent object. Across the RSC boundary a Server Component receives
7
+ a *client reference*, so `Live.Preview` would resolve to `undefined`.
8
+
9
+ ```ts
10
+ import { LiveProvider, LivePreview, defineLoader } from 'next-live';
11
+ import { LiveEditor } from 'next-live/editor'; // separate entry - see below
12
+ import { precompile } from 'next-live/server';
13
+ ```
14
+
15
+ Three entry points, so you only ship what you use:
16
+
17
+ | Entry | Contains | Why separate |
18
+ |---|---|---|
19
+ | `next-live` | Provider, preview, error, hooks, registry, engine | - |
20
+ | `next-live/editor` | `<LiveEditor>` | It is the only thing needing `prism-react-renderer`. Measured: a preview-only page pays 16.1 KB instead of 97.2 KB. `prism-react-renderer` is an **optional peer dependency**: npm never installs it automatically, so run `npm install prism-react-renderer` yourself if you use this entry. |
21
+ | `next-live/server` | `precompile`, `validateSnippet(s)` | Imports Sucrase statically; must never reach the client bundle. |
22
+
23
+ ## Components
24
+
25
+ ### `<LiveProvider>`
26
+
27
+ Compiles `code` and provides the result to its children. Everything else must be
28
+ inside it.
29
+
30
+ | Prop | Type | Default | Notes |
31
+ |---|---|---|---|
32
+ | `code` | `string` | - | The snippet. Controlled: change it and the preview follows. |
33
+ | `modules` | `ModuleRegistry` | `{}` | Specifier → value or loader. Merges over the built-ins. |
34
+ | `scope` | `LiveScope` | `{}` | Free variables injected as bare identifiers. |
35
+ | `props` | `Record<string, unknown>` | `{}` | Passed to the component **by reference**. |
36
+ | `fallback` | `ReactNode` | `null` | Rendered until the first compile finishes. |
37
+ | `language` | `string` | `'tsx'` | Highlighting hint for `<LiveEditor>`. |
38
+ | `onError` | `(error: Error) => void` | - | Called on every compile and runtime error. |
39
+ | `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. |
40
+ | `onCompileSuccess` | `(info: CompileSuccessInfo) => void` | - | Called after every successful compile with `compileId`, sorted `imports`, optional `via`, and `durationMs`. Not called on failure or abort. |
41
+ | `debounce` | `number` | `150` | Milliseconds before recompiling after a change. |
42
+ | `keepLastGood` | `boolean` | `true` | Keep the last working component mounted when a recompile fails. |
43
+ | `maxRendersPerSecond` | `number` | `1000` | Render-loop breaker threshold. |
44
+ | `transform` | `TransformFn` | - | Replace the built-in Sucrase pass (e.g. server-precompiled output). |
45
+ | `resolveSubpaths` | `boolean` | `false` | Resolve `pkg/Sub` against a registered `pkg` by property access. |
46
+ | `filePath` | `string` | `'LiveCode.tsx'` | Name shown in stack traces and DevTools. |
47
+ | `production` | `boolean` | `true` | `false` selects `react/jsx-dev-runtime` for richer stacks. |
48
+ | `jsxRuntime` | `'automatic' \| 'classic'` | `'automatic'` | |
49
+ | `jsxImportSource` | `string` | `'react'` | Register `<source>/jsx-runtime` if you change this. |
50
+
51
+ ### `<LivePreview>`
52
+
53
+ Renders the compiled component inside an error boundary.
54
+
55
+ | Prop | Type | Default | Notes |
56
+ |---|---|---|---|
57
+ | `props` | `Record<string, unknown>` | - | Merged over the provider's `props`. |
58
+ | `fallback` | `ReactNode` | provider's | Shown until the first compile finishes. |
59
+ | `as` | `ElementType` | `'div'` | Wrapper element. |
60
+ | `className` / `style` | | | Applied to the wrapper. |
61
+
62
+ ### `<LiveEditor>`
63
+
64
+ A `<textarea>` layered over syntax-highlighted output. No editor engine, so it
65
+ stays small and has no SSR quirks.
66
+
67
+ | Prop | Type | Default | Notes |
68
+ |---|---|---|---|
69
+ | `renderEditor` | `(props: LiveEditorRenderProps) => ReactNode` | - | Replace the built-in editor entirely. |
70
+ | `theme` | `PrismTheme` | `themes.vsDark` | From `prism-react-renderer`. |
71
+ | `prism` | `typeof Prism` | built-in | A Prism instance with extra languages registered. |
72
+ | `readOnly` | `boolean` | see below | Defaults to `true` when there is nothing to write edits to. |
73
+ | `tabSize` | `number` | `2` | Spaces inserted by the Tab key. |
74
+ | `padding` | `number` | `16` | |
75
+ | `errorLineStyle` | `CSSProperties \| null` | red inset highlight | Paint-only highlight on the error line. Pass `null` to disable. |
76
+ | `errorLineClassName` | `string` | - | Extra class on the error line. |
77
+ | `focusRingStyle` | `CSSProperties \| null` | 2px blue outline | Ring painted while the editor holds keyboard focus. |
78
+ | `aria-label` | `string` | `'Live code editor'` | Accessible name. |
79
+ | `code` | `string` | from context | Standalone mode - see below. |
80
+ | `onChange` | `(code: string) => void` | from context | Standalone mode - see below. |
81
+ | `language` | `string` | from context, then `'tsx'` | |
82
+ | `error` | `Error \| null` | from context | Error to underline. |
83
+ | `className` / `style` | | | |
84
+
85
+ `LiveEditorRenderProps` also exposes `error`, `errorLine`, and `errorColumn` for custom editors.
86
+
87
+ #### Standalone, without a provider
88
+
89
+ `<LiveEditor>` normally takes its code from the surrounding `<LiveProvider>`
90
+ and sends edits back to it. Pass `code` and it works on its own, which is how
91
+ you render a highlighted snippet on a page that is not running anything:
92
+
93
+ ```tsx
94
+ import { LiveEditor } from 'next-live/editor';
95
+
96
+ // Read-only: no onChange, so nothing can be written to.
97
+ <LiveEditor code={source} language="tsx" />
98
+
99
+ // Editable, driven by your own state.
100
+ <LiveEditor code={code} onChange={setCode} />
101
+ ```
102
+
103
+ With neither a provider nor `code`, the editor throws rather than rendering
104
+ empty.
105
+
106
+ #### Highlighting other languages
107
+
108
+ `prism-react-renderer` bundles a small language set. For anything else, hand
109
+ over a Prism instance you have extended:
110
+
111
+ ```tsx
112
+ import { Prism } from 'prism-react-renderer';
113
+
114
+ (globalThis as typeof globalThis & { Prism?: unknown }).Prism = Prism;
115
+ await import('prismjs/components/prism-rust');
116
+
117
+ <LiveEditor code={source} language="rust" prism={Prism} />
118
+ ```
119
+
120
+ #### Keyboard access
121
+
122
+ Tab inserts spaces, because an editor that moves focus on Tab cannot be typed
123
+ into. **Press Escape, then Tab, to move focus out** - the same convention
124
+ CodeMirror and Monaco use. The editor announces this through
125
+ `aria-keyshortcuts` and a visually-hidden description, and paints a focus ring
126
+ while it holds focus, so it satisfies WCAG 2.1.2 (No Keyboard Trap) and 2.4.7
127
+ (Focus Visible).
128
+
129
+ Any other keystroke re-arms indentation, so Escape only ever releases the very
130
+ next Tab.
131
+
132
+ Dropping in a different editor:
133
+
134
+ ```tsx
135
+ <LiveEditor
136
+ renderEditor={({ code, onChange, language, errorLine }) => (
137
+ <CodeMirror value={code} onChange={onChange} lang={language} highlightLine={errorLine} />
138
+ )}
139
+ />
140
+ ```
141
+
142
+ ### `<LiveError>`
143
+
144
+ Shows the current compile or runtime error; renders nothing when healthy.
145
+
146
+ | Prop | Type | Default |
147
+ |---|---|---|
148
+ | `children` | `(error: Error) => ReactNode` | built-in rendering |
149
+ | `as` | `ElementType` | `'pre'` |
150
+ | `className` / `style` | | |
151
+
152
+ ### `<LiveErrorBoundary>`
153
+
154
+ Used internally by `<LivePreview>`. Exported for custom UIs.
155
+
156
+ | Prop | Type | Notes |
157
+ |---|---|---|
158
+ | `onError` | `(error: Error) => void` | Required. |
159
+ | `resetKey` | `unknown` | Changing it clears the error. Wire to `compileId`. |
160
+ | `fallback` | `ReactNode` | Rendered while in the error state. |
161
+
162
+ ## Hooks
163
+
164
+ ### `useLiveRunner`
165
+
166
+ The headless engine, for building a completely custom UI.
167
+
168
+ ```ts
169
+ const { code, setCode, Component, element, error, isCompiling, compileId } =
170
+ useLiveRunner({ code: source, modules, scope });
171
+ ```
172
+
173
+ Accepts every `LiveProvider` option except `props`, `language`, `onError`, and
174
+ `fallback` - including `onCompileSuccess`. Returns:
175
+
176
+ | Field | Type | Notes |
177
+ |---|---|---|
178
+ | `code` | `string` | Current source. |
179
+ | `setCode` | `(code: string) => void` | Stable identity. |
180
+ | `Component` | `ComponentType \| null` | `null` until the first successful compile, including during SSR. |
181
+ | `element` | `ReactElement \| null` | Set instead of `Component` when the snippet produced an element. |
182
+ | `error` | `Error \| null` | |
183
+ | `isCompiling` | `boolean` | Only true after ~200 ms, so fast compiles never flash a spinner. |
184
+ | `compileId` | `number` | Increments on every successful compile. Use as a remount `key`. |
185
+
186
+ ### `useLiveModule`
187
+
188
+ Runs a snippet and returns its exports, for code that is not a component.
189
+
190
+ ```ts
191
+ const { exports, value, error, isCompiling, compileId } =
192
+ useLiveModule<PricingRule>({ code: source, modules });
193
+ ```
194
+
195
+ Accepts every `useLiveRunner` option except `maxRendersPerSecond` (nothing is
196
+ rendered, so there is no render loop to break). Returns `exports` (null until
197
+ the first successful run), `value` as shorthand for `exports?.default`, and the
198
+ same `error` / `isCompiling` / `compileId` fields.
199
+
200
+ The type parameter is a claim, not a check, validate the shape at runtime. See
201
+ [Non-UI snippets](./09-non-ui-snippets.md).
202
+
203
+ ### `useLiveContext`
204
+
205
+ Reads the surrounding `<LiveProvider>`, for custom editors, toolbars, or status
206
+ indicators. Throws if called outside a provider. Returns the `useLiveRunner`
207
+ fields plus `props`, `language`, `fallback`, and `reportRuntimeError`.
208
+
209
+ ## Registry helpers
210
+
211
+ ### `defineLoader(load)`
212
+
213
+ Marks a function as a lazy loader rather than the module value itself. Without
214
+ it, a registered component function would be indistinguishable from a loader.
215
+
216
+ ```ts
217
+ '@app/store': defineLoader(() => import('@/lib/store'))
218
+ ```
219
+
220
+ The loader receives the imported specifier - which is what prefix entries need:
221
+
222
+ ```ts
223
+ 'big-lib/': defineLoader((specifier) => import(`big-lib/${specifier.slice(8)}`))
224
+ ```
225
+
226
+ ### `defineModule({ default, exports })`
227
+
228
+ Builds an explicit module record. A registered object with its own `default` key
229
+ is normally unwrapped; use this when the whole object *is* the default export.
230
+ `default` and `exports.default` address the same slot.
231
+
232
+ ### `createRegistry(...groups)`
233
+
234
+ Merges registry groups, later groups winning. Warns in development when two
235
+ groups define the same key.
236
+
237
+ ### `registryFromGlob(glob, toSpecifier)`
238
+
239
+ Converts `import.meta.glob`'s lazy result into a registry of loaders.
240
+ `toSpecifier` maps a file path to the specifier authors write; return `null` to
241
+ omit a file. See [Scaling](./04-scaling.md#generate-entries-from-the-filesystem)
242
+ for the directory constraint.
243
+
244
+ ### `builtinModules`
245
+
246
+ The always-registered map: `react`, `react/jsx-runtime`, `react/jsx-dev-runtime`.
247
+
248
+ ## Engine
249
+
250
+ | Export | Purpose |
251
+ |---|---|
252
+ | `compile(input)` | Transpile + resolve + evaluate. Returns `{ renderable, via, code, imports }`. Throws if the snippet produced nothing renderable. |
253
+ | `compileModule(input)` | The same pipeline, returning `{ exports, code, imports }` with no component required. |
254
+ | `errorPosition(error)` | Reads `{ line, column? }` from a compile or runtime error, if present. |
255
+ | `transpile(source, options, transform?)` | Source → CommonJS. No evaluation. |
256
+ | `preloadTranspiler()` | Warm the Sucrase chunk during idle time. |
257
+ | `precompiledTransform(result)` | Wraps a server-precompiled result as a `transform`, so the client never loads Sucrase. Exported from the **client** entry - importing it from `next-live/server` would pull the transpiler into your page. |
258
+ | `setTranspiler(module)` | Swap the transpiler. For tests and custom backends. |
259
+ | `normalizeModule(value)` | The interop normalisation applied to registry values. |
260
+ | `createRequire(resolved)` | The synchronous `require` shim. |
261
+ | `resolveModules(options)` | Resolve specifiers against a registry. |
262
+ | `createRenderBudget(options)` | The render-loop breaker. |
263
+
264
+ ## Server entry: `next-live/server`
265
+
266
+ No `'use client'` directive and no React import, so it is safe in Route Handlers
267
+ and Server Components.
268
+
269
+ ### `precompile(source, options?)`
270
+
271
+ Transpiles to the same CommonJS the browser path produces. Returns a
272
+ `PrecompileResult`:
273
+
274
+ | Field | Type | Meaning |
275
+ |---|---|---|
276
+ | `code` | `string` | The transpiled CommonJS. |
277
+ | `hash` | `string` | Stable hash of the source and the options that affect output. Use it as a cache key or ETag. |
278
+ | `linePrefixOffset` | `number` | Lines the wrapper added above the snippet; needed to map error lines back. |
279
+ | `expression` | `boolean` | Whether the snippet was compiled as a bare expression rather than a module. |
280
+
281
+ `PrecompileResult extends TransformResult`, so a result can be handed straight
282
+ to [`precompiledTransform`](#engine).
283
+
284
+ ### `validateSnippet(source, options?)`
285
+
286
+ Statically checks that a snippet compiles and that every import resolves.
287
+ Never evaluates, so it is safe to run over untrusted content in CI.
288
+
289
+ ```ts
290
+ const result = validateSnippet(source, { modules: ['@app/store', 'big-lib/'] });
291
+ // { ok, issues: [{ kind, message, specifier?, suggestion?, line?, column? }], imports }
292
+ ```
293
+
294
+ `modules` accepts a registry object or just its keys.
295
+
296
+ Optional policy flags (all opt-in; defaults unchanged):
297
+
298
+ | Option | Notes |
299
+ |---|---|
300
+ | `maxSourceBytes` | Reject snippets over this UTF-8 byte count before transpile. |
301
+ | `forbidNodeBuiltins` | Treat `node:*` imports as forbidden. |
302
+ | `forbidRemoteImports` | Treat `https://`, `http://`, and `//` imports as forbidden. |
303
+ | `denySpecifiers` | Deny listed specifiers even when registered (prefix `/` denies a subtree). |
304
+
305
+ ### `validateSnippets(snippets, options?)`
306
+
307
+ Validates many at once and returns only the failures, as
308
+ `{ id, result }[]`. See [Validating in CI](./10-validating-in-ci.md).
309
+
310
+ ## Errors
311
+
312
+ All extend `LiveError` (exported as `LiveErrorBase` to avoid colliding with the
313
+ `<LiveError>` component).
314
+
315
+ | Class | Raised when |
316
+ |---|---|
317
+ | `LiveCompileError` | Parse/transpile failure, or CSP blocking `eval`. Carries `line` and `column`. |
318
+ | `LiveRuntimeError` | The snippet threw. Carries `line` where it can be mapped. |
319
+ | `RenderLoopError` | The render-rate breaker tripped. |
320
+ | `ModuleNotFoundError` | An import specifier is not registered. Carries `specifier` and `available`. |
321
+ | `NoComponentError` | The snippet produced nothing renderable. |
322
+ | `TranspilerLoadError` | Sucrase failed to load (usually a chunk-load failure). |
323
+
324
+ ## Types
325
+
326
+ `ModuleRegistry`, `ModuleLoader`, `ModuleValue`, `NormalizedModule`, `LiveScope`,
327
+ `LiveRenderable`, `LiveRunnerState`, `LiveContextValue`, `CompileOptions`,
328
+ `CompileResult`, `CompileInput`, `CompileSuccessInfo`, `TranspileOptions`,
329
+ `TransformFn`, `TransformResult`, `ExtractionSource`, `UseLiveRunnerOptions`,
330
+ `RenderBudgetOptions`, `GlobResult`, `CompileModuleResult`, `LiveModuleState`,
331
+ `UseLiveModuleOptions`, `PositionedError`, `ValidationResult`, `ValidationIssue`,
332
+ `ValidationIssueKind` (`syntax`, `unresolved-import`, `source-too-large`,
333
+ `forbidden-import`), `ValidateOptions`, plus the props type for each component.
334
+
335
+ ---
336
+
337
+ [← Security](./05-security.md) · [Docs index](./README.md) · [Troubleshooting →](./07-troubleshooting.md)
@@ -0,0 +1,289 @@
1
+ # Troubleshooting
2
+
3
+ [← API reference](./06-api-reference.md) · [Docs index](./README.md) · [Integration guide →](./08-integration-guide.md)
4
+
5
+ Every error below is one you can actually hit, with its real message text.
6
+
7
+ ## `LiveEditor` is not exported from `next-live`
8
+
9
+ ```
10
+ The requested module 'next-live' does not provide an export named 'LiveEditor'
11
+ ```
12
+
13
+ `<LiveEditor>` lives on its own entry point so preview-only pages never pay for
14
+ a syntax highlighter:
15
+
16
+ ```tsx
17
+ import { LiveProvider, LivePreview, LiveError } from 'next-live';
18
+ import { LiveEditor } from 'next-live/editor';
19
+ ```
20
+
21
+ Install the optional peer when you use the editor:
22
+
23
+ ```bash
24
+ npm install prism-react-renderer
25
+ ```
26
+
27
+ See [API reference: entry points](./06-api-reference.md#components).
28
+
29
+ ## `Module 'x' is not registered in the next-live scope`
30
+
31
+ ```
32
+ Module '@app/stroe' is not registered in the next-live scope.
33
+
34
+ Did you mean '@app/store'?
35
+
36
+ Registered modules (5): react, react/jsx-runtime, react/jsx-dev-runtime,
37
+ '@app/store', '@app/ui'
38
+ ```
39
+
40
+ The snippet imported something you did not register. The message lists what *is*
41
+ available and suggests the nearest match.
42
+
43
+ **If the name looks right but is still missing**, the usual cause is a registry
44
+ built by `registryFromGlob` that silently produced no entries - see
45
+ [the empty glob](#my-registry-is-empty-and-every-import-fails) below.
46
+
47
+ **An unused import never triggers this.** The TypeScript transform removes it
48
+ before resolution, exactly as `tsc` would.
49
+
50
+ ## My registry is empty and every import fails
51
+
52
+ `import.meta.glob` matched nothing and returned `{}`, no error, no warning.
53
+
54
+ **Cause:** the pattern points outside the calling file's own directory.
55
+ Turbopack resolves it relative to that file, and a `../` pattern silently
56
+ matches nothing.
57
+
58
+ ```ts
59
+ import.meta.glob('../modules/*.ts') // ❌ empty, no warning
60
+ import.meta.glob('./modules/*.ts') // ✅
61
+ ```
62
+
63
+ **Fix:** move the file that calls `import.meta.glob` so the directory it globs is
64
+ at or below it. Confirm with a quick `console.log(Object.keys(import.meta.glob(...)))`.
65
+
66
+ Also note `import.meta.glob` requires **Turbopack**; it does not exist under
67
+ webpack. Details in [Scaling](./04-scaling.md#the-directory-rule-that-will-cost-you-an-hour).
68
+
69
+ ## `next-live could not evaluate this snippet: the page's Content Security Policy blocks eval`
70
+
71
+ Your CSP is missing `'unsafe-eval'` on this route. That is expected -
72
+ `next-live` compiles at runtime.
73
+
74
+ **Fix:** add the route to your runner routes in `proxy.ts`. See
75
+ [Security](./05-security.md#3-scope-unsafe-eval-to-the-routes-that-run-snippets).
76
+
77
+ A nonce will not help: nonces authorize script *elements*, while `new Function`
78
+ is governed solely by `'unsafe-eval'`.
79
+
80
+ ## `This component rendered more than 1000 times in 1000ms`
81
+
82
+ ```
83
+ This component rendered more than 1000 times in 1000ms, so next-live stopped it
84
+ to keep the page responsive.
85
+
86
+ The usual causes are calling a state setter during render, or a useEffect that
87
+ updates state without a correct dependency array.
88
+ ```
89
+
90
+ The render-loop breaker tripped. Look in the snippet for:
91
+
92
+ ```tsx
93
+ setCount(n + 1); // ❌ during render
94
+ useEffect(() => setCount(n + 1)); // ❌ no dependency array
95
+ ```
96
+
97
+ The breaker stays tripped until the next compile - deliberately, because React
98
+ retries a failed render before handing the error to a boundary, so a breaker
99
+ that forgave itself would let every retry succeed and the loop would never
100
+ surface. Editing the snippet clears it.
101
+
102
+ **If it fires on correct code**, you are rendering faster than 1000 times a
103
+ second, plausible for animation-driven snippets. Raise the threshold:
104
+
105
+ ```tsx
106
+ <LiveProvider maxRendersPerSecond={5000} />
107
+ ```
108
+
109
+ ## `You're importing a component that needs useState`
110
+
111
+ Thrown by Next, not by `next-live`, when a Client Component is imported into a
112
+ Server Component without the `'use client'` directive surviving the build.
113
+
114
+ If you see this from the published package, the build output lost its directive.
115
+ Check:
116
+
117
+ ```bash
118
+ head -1 node_modules/next-live/dist/index.js # → "use client";
119
+ ```
120
+
121
+ If you see this from **your own** runner component, add `'use client'` to the top
122
+ of the file that renders `<LiveProvider>`.
123
+
124
+ ## My store state is not shared with the host app
125
+
126
+ The snippet has a *different copy* of your store module. Almost always two
127
+ installed versions:
128
+
129
+ ```bash
130
+ npm ls zustand # or whichever library
131
+ ```
132
+
133
+ More than one version means two modules and two stores. Run `npm dedupe`, or pin
134
+ one version with `overrides`. Full explanation and the other three causes in
135
+ [Sharing libraries](./03-sharing-your-app-libraries.md#when-you-really-do-get-two-copies).
136
+
137
+ ## `Invalid hook call` / "more than one copy of React"
138
+
139
+ Same root cause as above, applied to React. Snippets use **your** React instance,
140
+ so two Reacts in `node_modules` break hooks:
141
+
142
+ ```bash
143
+ npm ls react
144
+ ```
145
+
146
+ ## My TypeScript errors are not reported
147
+
148
+ Expected. Sucrase **strips** types without checking them, which is what keeps
149
+ compilation in the single-digit milliseconds. A snippet with a real type error
150
+ compiles cleanly and fails at runtime.
151
+
152
+ Most runtime transpilers make the same trade-off. If authors need real diagnostics, run
153
+ `tsc` or the TypeScript language service in a worker on your side and surface the
154
+ results yourself; `next-live` does not do this for you.
155
+
156
+ ## `The snippet did not produce a component`
157
+
158
+ ```
159
+ The snippet did not produce a component. Add `export default YourComponent`,
160
+ or end the snippet with a single JSX expression.
161
+ ```
162
+
163
+ The code ran but nothing renderable came out. Add an explicit default export -
164
+ that is the supported, unambiguous form:
165
+
166
+ ```tsx
167
+ export default function App() { return <div/>; }
168
+ ```
169
+
170
+ Related variants:
171
+
172
+ - *"The default export is a string, which React cannot render"* - you exported a
173
+ value rather than a component.
174
+ - *"Several components were exported and none is the default"* - mark one with
175
+ `export default`.
176
+
177
+ ## The preview flashes or disappears while typing
178
+
179
+ It should not, a failed recompile keeps the last working component mounted.
180
+ If you turned that off (`keepLastGood={false}`), turn it back on.
181
+
182
+ If the preview *remounts* and loses state on every successful compile, that is
183
+ expected: a recompiled component is a new function identity, so React cannot
184
+ carry state over.
185
+
186
+ ## Hydration errors (#418 / #425)
187
+
188
+ `next-live` should never cause these, it renders the same `fallback` on the
189
+ server and on the client's first pass.
190
+
191
+ If you see one, check whether *your* runner component renders something
192
+ different between server and client, for example `Date.now()` or
193
+ `window.matchMedia`, outside of `next-live`.
194
+
195
+ ## Precompile ignores my edits
196
+
197
+ You enabled server precompile and passed `precompiledTransform(compiled)` as
198
+ `transform`, but editing the snippet no longer updates the preview.
199
+
200
+ **Cause:** `precompiledTransform` returns a constant closure. It always serves
201
+ the server-compiled output regardless of what `code` says now.
202
+
203
+ **Fix:** only apply the transform while `code` still matches the catalog source
204
+ that was precompiled. As soon as the author edits, set `transform={undefined}`
205
+ (or re-precompile the new source):
206
+
207
+ ```tsx
208
+ const usingPrecompile =
209
+ precompileEnabled && compiled && code === catalogSource;
210
+
211
+ <LiveProvider
212
+ code={code}
213
+ transform={usingPrecompile ? precompiledTransform(compiled) : undefined}
214
+ />
215
+ ```
216
+
217
+ See [Scaling: compile cost](./04-scaling.md#compile-cost-and-skipping-the-transpiler).
218
+
219
+ ## Tailwind classes in my snippet do nothing
220
+
221
+ Tailwind scans your **host** source files at build time. Utility classes written
222
+ only inside stored snippet strings are invisible to the scanner, so no CSS is
223
+ generated for them.
224
+
225
+ **Fix (recommended):** expose pre-built components through the registry:
226
+
227
+ ```ts
228
+ // lib/live-sdk/modules/ui.ts
229
+ export { Button, Card } from '@/components/ui';
230
+ ```
231
+
232
+ ```tsx
233
+ // snippet
234
+ import { Button, Card } from '@app/ui';
235
+ export default () => <Card><Button>Save</Button></Card>;
236
+ ```
237
+
238
+ The components' classes are compiled into your host bundle. This is what the
239
+ `/apps` shell demo does with shadcn.
240
+
241
+ **Alternative:** add a `@source` directive in your global CSS pointing at a
242
+ file that contains the utility class names your snippets use, or safelist them
243
+ in your Tailwind config. See the playground's `app/globals.css` for an example.
244
+
245
+ ## Do React hooks work in snippets?
246
+
247
+ Yes. Snippets import the **host's** React instance (`react` is a built-in
248
+ module), so `useState`, `useEffect`, `useContext`, and the rest work normally.
249
+ The `/apps` shell demo includes timer and fetch examples.
250
+
251
+ Common pitfalls are the same as in any React app: missing effect cleanup,
252
+ state updates during render, and dependency arrays. The render-loop breaker catches
253
+ runaway re-renders - see [above](#this-component-rendered-more-than-1000-times-in-1000ms).
254
+
255
+ ## The first compile is slow
256
+
257
+ The Sucrase chunk is fetched on first use. Warm it during idle time:
258
+
259
+ ```tsx
260
+ import { preloadTranspiler } from 'next-live';
261
+ useEffect(() => preloadTranspiler(), []);
262
+ ```
263
+
264
+ Or skip it entirely by precompiling on the server - see
265
+ [Scaling](./04-scaling.md#compile-cost-and-skipping-the-transpiler).
266
+
267
+ ## A snippet hung the whole tab
268
+
269
+ A synchronous infinite loop, `while (true) {}`, a runaway recursion, a
270
+ catastrophic regex. This **cannot** be interrupted: JavaScript offers no way to
271
+ stop synchronous code in its own realm, so no timer or `AbortController` will
272
+ fire. The tab must be closed.
273
+
274
+ The render-loop breaker catches the *asynchronous* variety (`setState` loops),
275
+ which is the common one in practice. See
276
+ [Security](./05-security.md#what-is-contained-and-what-is-not).
277
+
278
+ ## Still stuck
279
+
280
+ Useful things to capture before reporting an issue:
281
+
282
+ - The snippet source that reproduces it.
283
+ - The registry keys: `console.log(Object.keys(liveModules))`.
284
+ - Whether it happens in dev, production, or both, CSP differs between them.
285
+ - `npm ls react next-live`.
286
+
287
+ ---
288
+
289
+ [← API reference](./06-api-reference.md) · [Docs index](./README.md) · [Integration guide →](./08-integration-guide.md)