react-code-canvas
Version:
A browser-based canvas for rendering React components at runtime
301 lines (233 loc) • 13.4 kB
Markdown
---
name: react-code-canvas
metadata:
version: 0.0.3
library-version: "^5.0.0"
description: "Constraints for authoring React component code that will be rendered by react-code-canvas (ReactCanvas / EditTextReactCanvas). Use whenever generating, editing, reviewing, or validating a code string that gets passed to the canvas as its `code` prop — including LLM prompt construction in the host app. Covers the injected scope, forbidden patterns, entry shape, name collisions, performance rules, and programmatic validation."
---
# react-code-canvas — authoring constraints
Code passed to the canvas is **not** an ordinary module. It is transpiled with sucrase
and executed via `new Function(...scopeKeys, code)`, with dependencies handed in as
function parameters rather than resolved as imports.
Everything below follows from that. Violations mostly fail at *runtime*, inside the
rendered page, not at build time — so they are easy to ship unnoticed.
## 1. Entry shape
The canvas needs exactly one of:
```jsx
export default function Page() { return <div/>; } // preferred
```
```jsx
render(<Page />); // also supported
```
Neither present → the code compiles, produces nothing, and the canvas renders blank.
It reports `Code did not render anything` via `onError`, but nothing is displayed.
A bare leading expression also works — `<div/>`, `function(`, `()=>`, `class ` at the
start of the source get an implicit `export default` prepended. Do not rely on this;
be explicit.
## 2. No import statements
```jsx
import { useState } from 'react'; // ❌ throws `Module not found: 'react'`
import { motion } from 'motion/react'; // ❌
```
An import whose binding is **used** becomes a `require(...)` call, and the canvas
provides no modules to require — the whole component throws `Module not found` the
moment it evaluates. An import whose binding is never used is elided entirely and
does no harm (which is why the analyzer reports `import-statement` as non-fatal).
Either way: everything you need is already a global. Use it directly:
```jsx
export default function Page() {
const [open, setOpen] = useState(false); // ✅ already in scope
return <motion.div animate={{ opacity: 1 }} />;
}
```
## 3. What is in scope
| Group | Names | Load cost when referenced |
|---|---|---|
| React | `React`, `useState`, `useEffect`, `useContext`, `useReducer`, `useRef`, `useMemo`, `useCallback` | free (base) |
| Icons (preferred) | all **5841** `lucide-react` exports, e.g. `<Activity/>`, `<User/>`, `<ChevronRight/>` | ~0.8 KB gz per icon |
| Icons (legacy) | all **1611** `react-icons/fa` exports, e.g. `<FaUser/>`, `<FaHome/>` | **whole pack, ~420 KB gz** — one `Fa*` name fetches all of it |
| Charts | all **101** `recharts` exports, e.g. `ResponsiveContainer`, `LineChart`, `XAxis`, `CartesianGrid` | whole library, ~145 KB gz |
| Animation | `motion` (as `<motion.div>`) plus motion's **383** hooks/components | whole library, ~61 KB gz |
| Head | `Helmet`, `HelmetProvider` | free (base) |
| Entry | `render`, `exports`, `require` | free (base) |
Prefer lucide for icons in new code. The `Fa*` set exists so pages written before
the lucide switch keep rendering — see [performance](references/performance.md)
for why one stray `Fa*` name is the single most expensive token you can emit.
Plus every normal browser/JS global — `document`, `window`, `fetch`, `localStorage`,
`setTimeout`, `Math`, `JSON`, `Intl`, and so on. These all work.
### Names must appear literally in the source
The canvas does not ship those libraries up front. It scans your code for
identifiers and loads only what it finds — one file per icon, and recharts or motion
only if you reference them. Which means **a name the source never spells out is
never loaded**:
```jsx
const Icon = dark ? Moon : Sun; // ✅ both names appear literally
const which = 'Activity';
const Icon = globalThis[which]; // ❌ nothing to find; undefined at runtime
```
Writing names out in full is the normal thing to do, so this rarely bites. Building
component names by string concatenation never worked here anyway — the scope arrives
as `new Function` parameters, not as an object you can index.
### Not in scope
No other library is available. No `lodash`, no `axios`, no `ReactDOM`, no `Babel`,
no icon set beyond lucide and `react-icons/fa`. If the host app injects extra globals
via the `scope` prop, they are available too — but do not assume any beyond the
table above.
## 4. Name collisions — the sharpest edge
The scope draws on ~7850 flat global names. Declaring a component or variable with a
name that already exists silently shadows it, or worse, your JSX resolves to a chart
primitive.
Loading on demand does **not** save you here: mentioning `Activity` anywhere is what
puts `Activity` in scope, so `const Activity = ...` at module level is still a
duplicate declaration.
**Recharts owns these generic words:**
`Text` `Label` `Legend` `Tooltip` `Line` `Bar` `Cell` `Area` `Pie` `Customized`
`Brush` `Cross` `Curve` `Dot` `Polygon` `Rectangle` `Sector`
So `<Text>hello</Text>` is a *chart* component, not a text element. Use them only
inside a chart tree. For generic UI use plain tags (`<span>`, `<p>`) or a distinctly
named component.
**Lucide owns most common UI nouns.** Of 70 everyday component names checked
(`Navigation`, `Menu`, `Image`, `Link`, `Table`, `Search`, `Layout`, `Grid`, `Map`,
`Card`-adjacent words, …), **68 collide** with a lucide icon.
Therefore: **prefix your own components.** `SiteNavigation`, not `Navigation`.
`ProductCard`, not `Card`. `PageHeader`, not `Header`.
Never declare a module-scope name matching an injected global — including React hooks,
lucide icons, recharts components, motion exports, or the editing helpers
(`EditableText`, `__applyEditableTextPatch`).
## 5. Styling
**Default to inline `style` (or a `<style>` tag in the component).** The canvas
ships no CSS framework, so in-code styles are the only styling that works
everywhere, on every host, with zero extra download.
Tailwind classes work **only if the host page provides Tailwind** — and the usual
way hosts do that for arbitrary generated classes is the Play CDN, a ~124 KB
script that JIT-compiles CSS in the browser on every visit. Absent it,
`className` values are inert and the component renders unstyled. So Tailwind in
canvas code is both a hard host dependency and a per-visit performance cost that
inline styles simply don't have.
Use Tailwind only when the host explicitly guarantees it; never mix — a page
half-styled by each is the worst of both.
```jsx
// ✅ self-contained, styled on any host
<div style={{ display: 'flex', gap: 12, padding: '2rem' }}>
// ⚠️ renders unstyled unless the host loads Tailwind
<div className="flex gap-3 p-8">
```
For pseudo-classes, media queries, and keyframes — the things inline `style`
cannot express — emit one `<style>` tag inside the component:
```jsx
export default function PricingPage() {
return (
<main>
<style>{`
.pp-card { transition: transform .2s; }
.pp-card:hover { transform: translateY(-4px); }
@media (max-width: 640px) { .pp-grid { grid-template-columns: 1fr; } }
`}</style>
<div className="pp-grid" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<div className="pp-card">…</div>
</div>
</main>
);
}
```
Prefix such class names (`pp-`) like component names — the document is shared
with the host page.
## 6. Behavior worth designing around
**Empty code renders nothing** — no output, no loader. Not an error.
**Errors appear in a toast** pinned top-right, with the message in `#react-code-error`.
**A failed edit re-executes the previous good code.** The canvas keeps the last
successful render on screen while showing the error, and doing so re-runs that code's
module-level side effects. Guard anything that must happen once:
```jsx
if (!document.getElementById('my-script')) {
// inject once
}
```
**Module-level code runs on every evaluation.** Keep top-level work cheap; heavy
synchronous work there blocks the main thread and freezes the page.
**Evaluation is synchronous.** `render()` must be called in the same tick —
`setTimeout(() => render(<X/>), 0)` never renders.
**The first render is not.** The canvas fetches the icons/charts/animation your code
references before it can evaluate anything, so the first frame is empty and the
loading overlay is shown whenever that loading spans a paint (with everything
already cached, resolution can land in a microtask and no overlay frame appears).
Evaluation itself is still synchronous once the scope has landed; it just cannot
start on the first commit. Nothing to do about this while authoring — it only
matters if you are writing tests against the canvas, which must await the render
rather than assert immediately.
## 7. Performance
Every name in the source decides what gets downloaded, and the whole string is
re-evaluated on every change — so authoring choices are directly load-time and
main-thread choices. The rules that matter, in order of cost:
1. **Never emit an `Fa*` name unless the page already uses Font Awesome.** One
`Fa*` reference fetches the whole ~420 KB gz pack; the lucide equivalent is
one ~0.8 KB file. `FaUser` → `User`, `FaHome` → `House`.
2. **Do not mention chart/animation names you don't render.** The scanner is a
regex over the whole source — `recharts` (~145 KB gz) and `motion` (~61 KB gz)
load even when the name only appears in a **comment or string literal**.
`// like a LineChart` costs 145 KB.
3. **Keep module level empty.** Top-level statements re-run on every evaluation,
including the re-execution after a failed edit. Constants are fine; work is not.
4. **Render the shell before the data.** The canvas paints as soon as evaluation
returns — a component that returns `null` until a `fetch` resolves wastes that
paint and pushes the host page's LCP onto your network call.
5. **Style inline, not with Tailwind.** Tailwind classes only render if the host
ships a ~124 KB browser JIT on every visit; inline `style` (plus a `<style>`
tag for hover/media/keyframes) costs nothing and works everywhere. See .5.
Full guidance with copy-paste patterns:
- [references/performance.md](references/performance.md) — the rules above in
depth, with the internals that explain them
- [references/patterns.md](references/patterns.md) — known-good page skeletons
(static page, chart page, animated page, data-driven page)
## 8. Security
Code is evaluated in the **host page's own context** — same origin, full access to
`document`, `window`, cookies, `localStorage`, and authenticated `fetch`. There is no
iframe sandbox. Never pass untrusted or unreviewed code to the canvas without adding
isolation appropriate to your application.
## 9. Validate programmatically
Do not rely on review alone. The library ships a static analyzer that runs in Node —
no DOM, no React render — suitable for CI, batch audits, and migrations:
```js
import { analyzeReactCode } from 'react-code-canvas';
const result = await analyzeReactCode(code);
// {
// valid: boolean,
// issues: [{ type, message, name?, line?, column? }],
// unknownGlobals: string[], // names that will throw ReferenceError
// referencedGlobals: string[],
// hasDefaultExport: boolean,
// }
```
Issue types: `empty`, `syntax`, `transform`, `compile`, `import-statement`,
`unknown-identifier`, `no-default-export`. Only `import-statement` is non-fatal (the
code may still render if the binding is never used).
`compile` is the one worth knowing about: scope entries become `new Function`
parameters, so a module-scope `const useState = 1` is a duplicate declaration — valid
standalone JS that parses fine but throws in the canvas. That is .4 caught mechanically.
Options:
```js
await analyzeReactCode(code, {
// Omit to check against exactly what the canvas would load for THIS code.
// Passing one replaces that, so lucide/recharts/motion stop being available.
scope,
allowedGlobals: ['MY_FLAG'],// extra host-injected names
forbidImports: true, // default
});
```
It catches syntax errors, references to names nothing provides (this is how you find
stored code still using `Fa*` icons), and code that compiles but produces no
component. It does **not** execute the code, so it cannot catch logic errors or
failures that only surface once mounted. For that, render it with `ReactCanvas` and
watch its `onError` callback.
## Checklist
- [ ] default export (or `render()` call) present
- [ ] zero `import` statements
- [ ] own components prefixed so they cannot shadow the ~7850 injected globals
- [ ] every scope name spelled out literally, never built from strings
- [ ] `Text`/`Label`/`Tooltip`/`Line`/`Bar`/etc. used only inside chart trees
- [ ] top-level side effects guarded against re-execution
- [ ] lucide icons, not `Fa*`, unless the page already pays for Font Awesome
- [ ] no recharts/motion names in comments or strings unless the page charts/animates
- [ ] static shell renders before any data fetch resolves
- [ ] styled with inline `style` / in-code `<style>`, not Tailwind (unless the host guarantees it)
- [ ] `analyzeReactCode()` returns `valid: true`