A demo showing how to pre-render multiple code snippets and switch between them via ?lang= query params — no skeleton loaders, no flash. Uses a tiny invisible Suspense boundary for URL sync, but all content is fully pre-rendered.
Inspired by A Clock That Doesn't Snap by Ethan Niser.
- Server renders all code blocks (ts, js, python, rust) using Shiki with
"use cache" - CSS hides all except the default (TypeScript) via parent
data-langattribute - Inline script runs before React hydrates — reads
?lang=from URL, setsdata-langon container - CSS instantly shows the correct panel — no flash, no loading state
- React hydrates — useState reads the same URL, matches the DOM, no hydration mismatch
- LangSyncer watches URL — any navigation to
?lang=Xupdates all[data-lang-sync]elements
A valid and common approach is to render a static shell with a skeleton loader while fetching/computing the dynamic content. This works well when:
- Content has predictable dimensions
- A brief loading state is acceptable
- You want simpler architecture
However, skeletons struggle when content height varies significantly — like code snippets across languages. Our Fibonacci example:
- TypeScript: ~15 lines
- Python: ~10 lines
- Rust: ~20 lines
A fixed-height skeleton would either:
- Cut off longer snippets (bad)
- Leave empty space for shorter ones (jarring)
- Cause layout shift when real content loads (worst)
By pre-rendering all variants and using CSS to switch, we get:
- Zero layout shift
- Instant switching
- Correct height from first paint
We use codeToHast + hast-util-to-jsx-runtime to convert Shiki's output to proper React elements:
const hast = await codeToHast(code, { lang, theme });
const element = toJsxRuntime(hast, { jsx, jsxs, Fragment });Pros:
- No
dangerouslySetInnerHTML - Safe by default — no XSS risk
- Proper React tree — works with React DevTools, etc.
Cons:
- More DOM nodes (React adds some overhead)
- Slightly larger payload
const html = await codeToHtml(code, { lang, theme });
// ...
<div dangerouslySetInnerHTML={{ __html: html }} />Pros:
- Simpler code
- Smaller payload
- Fewer DOM nodes
Cons:
- Must sanitize if code comes from user input — never trust user input
- Bypasses React's DOM management
For trusted content (like static code snippets), the HTML approach is fine. For user-generated content, use the React elements approach or sanitize with a library like DOMPurify.
Wrap multiple <CodeSwitcher /> components in a <LangProvider> — they all sync to the same ?lang= param:
<LangProvider>
<CodeSwitcher /> {/* Fibonacci */}
<CodeSwitcher /> {/* QuickSort */}
<CodeSwitcher /> {/* Binary Search */}
</LangProvider>Any element with data-lang-sync attribute will be synced when the URL changes.
The LangProvider includes an invisible LangSyncer component wrapped in Suspense:
<Suspense fallback={null}>
<LangSyncer /> {/* uses useSearchParams, syncs DOM */}
</Suspense>This means:
- Any link that navigates to
?lang=Xwill update all code blocks - No need to wire up custom click handlers
- The Suspense boundary is invisible (
fallback={null}) — no skeleton
This demo uses Next.js Cache Components (cacheComponents: true) with the "use cache" directive:
async function highlightAll() {
"use cache";
// expensive Shiki highlighting happens here
}The highlighted code is cached and revalidated every 15 minutes, avoiding redundant work on each request.
pnpm install
pnpm devThen visit:
- http://localhost:3000 → TypeScript (default)
- http://localhost:3000?lang=js → JavaScript
- http://localhost:3000?lang=python → Python
- http://localhost:3000?lang=rust → Rust