Sandpack Patterns
Quick Start Template
Most examples are single-file. Copy this and modify:
<Sandpack>
` ` `js
import { useState } from 'react';
export default function Example() {
const [value, setValue] = useState(0);
return (
<button onClick={() => setValue(value + 1)}>
Clicked {value} times
</button>
);
}
` ` `
</Sandpack>File Naming
| Pattern | Usage |
|---|---|
``` `js ``` | Main file (no prefix) |
``` `js src/FileName.js ``` | Supporting files |
``` `js src/File.js active ``` | Active file (reference pages) |
``` `js src/data.js hidden ``` | Hidden files |
``` `css ``` | CSS styles |
``` `json package.json ``` | External dependencies |
Critical: Main file must have export default.
Line Highlighting
function Example() { // Lines 2-4 // will be // highlighted return null; }
## Code References (numbered callouts)
// Creates numbered markers pointing to "age" and "setAge" on line 4Expected Errors (intentionally broken examples)
// Line 7 shows as expected error
## Multi-File Example
<Sandpack>
import Gallery from './Gallery.js';
export default function App() {
return <Gallery />;
}export default function Gallery() {
return <h1>Gallery</h1>;
}h1 { color: purple; }External Dependencies
<Sandpack>
import { useImmer } from 'use-immer'; // ...
{ "dependencies": { "immer": "1.7.3", "use-immer": "0.5.1", "react": "latest", "react-dom": "latest", "react-scripts": "latest" } }
## Code Style in Sandpack (Required)
Sandpack examples are held to strict code style standards:
1. **Function declarations** for components (not arrows)
2. **`e`** for event parameters
3. **Single quotes** in JSX
4. **`const`** unless reassignment needed
5. **Spaces in destructuring**: `({props})` not `({props})`
6. **Two-line createRoot**: separate declaration and render call
7. **Multiline if statements**: always use braces
### Don't Create Hydration Mismatches
Sandpack examples must produce the same output on server and client:
// 🚫 This will cause hydration warnings export default function App() { const isClient = typeof window !== 'undefined'; return <div>{isClient ? 'Client' : 'Server'}</div>; }
### Use Ref for Non-Rendered State
// 🚫 Don't trigger re-renders for non-visual state const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []);
// ✅ Use ref instead const mounted = useRef(false); useEffect(() => { mounted.current = true; }, []);
## forwardRef and memo Patterns
### forwardRef - Use Named Function
// ✅ Named function for DevTools display name const MyInput = forwardRef(function MyInput(props, ref) { return <input {...props} ref={ref} />; });
// 🚫 Anonymous loses name const MyInput = forwardRef((props, ref) => { ... });
### memo - Use Named Function
// ✅ Preserves component name const Greeting = memo(function Greeting({ name }) { return <h1>Hello, {name}</h1>; });
## Line Length
- Prose: ~80 characters
- Code: ~60-70 characters
- Break long lines to avoid horizontal scrolling
## Anti-Patterns
| Pattern | Problem | Fix |
| --- | --- | --- |
| `const Comp = () => {}` | Not standard | `function Comp() {}` |
| `onClick={(event) =>...}` | Conflicts with global | `onClick={(e) =>...}` |
| `useState` for non-rendered values | Re-renders | Use `useRef` |
| Reading `window` during render | Hydration mismatch | Check in useEffect |
| Single-line if without braces | Harder to debug | Use multiline with braces |
| Chained `createRoot().render()` | Less clear | Two statements |
| `//...` without space | Inconsistent | `//...` with space |
| Tabs | Inconsistent | 2 spaces |
| `ReactDOM.render` | Deprecated | Use `createRoot` |
| Fake package names | Confusing | Use `'./your-storage-layer'` |
| `PropsWithChildren` | Outdated | `children?: ReactNode` |
| Missing `key` in lists | Warnings | Always include key |
## Additional Code Quality Rules
### Always Include Keys in Lists
// ✅ Correct {items.map(item => <li key={item.id}>{item.name}</li>)}
// 🚫 Wrong - missing key {items.map(item => <li>{item.name}</li>)}
### Use Realistic Import Paths
// ✅ Correct - descriptive path import { fetchData } from './your-data-layer';
// 🚫 Wrong - looks like a real npm package import { fetchData } from 'cool-data-lib';
### Console.log Labels
// ✅ Correct - labeled for clarity console.log('User:', user); console.log('Component Stack:', errorInfo.componentStack);
// 🚫 Wrong - unlabeled console.log(user);
### Keep Delays Reasonable
// ✅ Correct - 1-1.5 seconds setTimeout(() => setLoading(false), 1000);
// 🚫 Wrong - too long, feels sluggish setTimeout(() => setLoading(false), 3000);
## Updating Line Highlights
When modifying code in examples with line highlights (`{2-4}`), **always update the highlight line numbers** to match the new code. Incorrect line numbers cause rendering crashes.
## File Name Conventions
- Capitalize file names for component files: `Gallery.js` not `gallery.js`
- After initially explaining files are in `src/`, refer to files by name only: `Gallery.js` not `src/Gallery.js`
## Naming Conventions in Code
**Components:** PascalCase
- `Profile`, `Avatar`, `TodoList`, `PackingList`
**State variables:** Destructured pattern
- `const [count, setCount] = useState(0)`
- Booleans: `[isOnline, setIsOnline]`, `[isPacked, setIsPacked]`
- Status strings: `'typing'`, `'submitting'`, `'success'`, `'error'`
**Event handlers:**
- `handleClick`, `handleSubmit`, `handleAddTask`
**Props for callbacks:**
- `onClick`, `onChange`, `onAddTask`, `onSelect`
**Custom Hooks:**
- `useOnlineStatus`, `useChatRoom`, `useFormInput`
**Reducer actions:**
- Past tense: `'added'`, `'changed'`, `'deleted'`
- Snake_case compounds: `'changed_selection'`, `'sent_message'`
**Updater functions:** Single letter
- `setCount(n => n + 1)`
### Pedagogical Code Markers
**Wrong vs right code:**
// 🔴 Avoid: redundant state and unnecessary Effect // ✅ Good: calculated during rendering
**Console.log for lifecycle teaching:**
console.log('✅ Connecting...'); console.log('❌ Disconnected.');
### Server/Client Labeling
// Server Component async function Notes() { const notes = await db.notes.getAll(); }
// Client Component "use client" export default function Expandable({children}) { const [expanded, setExpanded] = useState(false); }
### Bundle Size Annotations
import marked from 'marked'; // 35.9K (11.2K gzipped) import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)
---
## Sandpack Example Guidelines
### Package.json Rules
**Include package.json when:**
- Using external npm packages (immer, remarkable, leaflet, toastify-js, etc.)
- Demonstrating experimental/canary React features
- Requiring specific React versions (`react: beta`, `react: 19.0.0-rc-*`)
**Omit package.json when:**
- Example uses only built-in React features
- No external dependencies needed
- Teaching basic hooks, state, or components
**Always mark package.json as hidden:**
{
"dependencies": {
"react": "latest",
"react-dom": "latest",
"react-scripts": "latest",
"immer": "1.7.3"
}
}**Version conventions:**
- Use `"latest"` for stable features
- Use exact versions only when compatibility requires it
- Include minimal dependencies (just what the example needs)
### Hidden File Patterns
**Always hide these file types:**
| File Type | Reason |
|-----------|--------|
| `package.json` | Configuration not the teaching point |
| `sandbox.config.json` | Sandbox setup is boilerplate |
| `public/index.html` | HTML structure not the focus |
| `src/data.js` | When it contains sample/mock data |
| `src/api.js` | When showing API usage, not implementation |
| `src/styles.css` | When styling is not the lesson |
| `src/router.js` | Supporting infrastructure |
| `src/actions.js` | Server action implementation details |
**Rationale:**
- Reduces cognitive load
- Keeps focus on the primary concept
- Creates cleaner, more focused examples
**Example:**export const items = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
];### Active File Patterns
**Mark as active when:**
- File contains the primary teaching concept
- Learner should focus on this code first
- Component demonstrates the hook/pattern being taught
**Effect of the `active` marker:**
- Sets initial editor tab focus when Sandpack loads
- Signals "this is what you should study"
- Works with hidden files to create focused examples
**Most common active file:** `src/index.js` or `src/App.js`
**Example:**// This file will be focused when example loads
export default function App() {
// ...
}### File Structure Guidelines
| Scenario | Structure | Reason |
|----------|-----------|--------|
| Basic hook usage | Single file | Simple, focused |
| Teaching imports | 2-3 files | Shows modularity |
| Context patterns | 4-5 files | Realistic structure |
| Complex state | 3+ files | Separation of concerns |
**Single File Examples (70% of cases):**
- Use for simple concepts
- 50-200 lines typical
- Best for: Counter, text inputs, basic hooks
**Multi-File Examples (30% of cases):**
- Use when teaching modularity/imports
- Use for context patterns (4-5 files)
- Use when component is reused
**File Naming:**
- Main component: `App.js` (capitalized)
- Component files: `Gallery.js`, `Button.js` (capitalized)
- Data files: `data.js` (lowercase)
- Utility files: `utils.js` (lowercase)
- Context files: `TasksContext.js` (named after what they provide)
### Code Size Limits
- Single file: **<200 lines**
- Multi-file total: **150-300 lines**
- Main component: **100-150 lines**
- Supporting files: **20-40 lines each**
### CSS Guidelines
**Always:**
- Include minimal CSS for demo interactivity
- Use semantic class names (`.panel`, `.button-primary`, `.panel-dark`)
- Support light/dark themes when showing UI concepts
- Keep CSS visible (never hidden)
**Size Guidelines:**
- Minimal (5-10 lines): Basic button styling, spacing
- Medium (15-30 lines): Panel styling, form layouts
- Complex (40+ lines): Only for layout-focused examples