React, Preact & Solid
bext provides SSR adapters that let you render React, Preact, or Solid components to HTML strings inside your template sites. This lets you use existing component libraries alongside bext's built-in JSX without rewriting them.
Each adapter wraps the framework's synchronous renderToString function for use inside bext's render worker pool.
React adapter
The React adapter renders React components to HTML strings using react-dom/server.
Setup
Add React to your project dependencies:
bun add react react-dom
bun add -d @types/react @types/react-dom
Usage
import { renderReact } from "@bext-stack/framework/react";
// A standard React component
function PriceChart({ data }: { data: number[] }) {
return (
<div className="chart">
{data.map((value, i) => (
<div
key={i}
className="bar"
style={{ height: `${value}%`, width: '20px', background: '#06b6d4' }}
/>
))}
</div>
);
}
// Render to HTML string
const chartHtml = renderReact(PriceChart, { data: [20, 45, 80, 35, 60] });
Embedding in bext JSX templates
Use dangerouslySetInnerHTML to embed React output in your bext JSX layout:
// server/template.tsx — bext JSX template
import { renderReact } from "@bext-stack/framework/react";
import { PriceChart } from "./components/PriceChart";
function ProductPage({ product }: { product: Product }) {
return (
<div className="product-detail">
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* React component rendered to HTML */}
<div dangerouslySetInnerHTML={{
__html: renderReact(PriceChart, { data: product.priceHistory })
}} />
</div>
);
}
API
/** Render a React component to an HTML string. */
function renderReact(component: any, props?: Record<string, any>): string;
/** Render an already-created React element to a string. */
function renderReactElement(element: any): string;
Preact adapter
Preact is a 3KB alternative to React with the same API. Use it when you need React-compatible components without the 7.5MB bundle overhead.
Setup
bun add preact preact-render-to-string
Usage
import { renderPreact, renderPreactComponent, h } from "@bext-stack/framework/preact";
// Using Preact's h() function
function Badge({ text, color }: { text: string; color: string }) {
return h("span", {
class: "badge",
style: `background:${color}; padding:2px 8px; border-radius:4px; color:white; font-size:12px`
}, text);
}
// Render a component with props
const badgeHtml = renderPreactComponent(Badge, { text: "New", color: "#10b981" });
// <span class="badge" style="background:#10b981; padding:2px 8px; border-radius:4px; color:white; font-size:12px">New</span>
// Or render a VNode directly
const html = renderPreact(h(Badge, { text: "Sale", color: "#f43f5e" }));
With Preact JSX
If you configure jsxImportSource: "preact" in a separate tsconfig for your Preact components, you can use JSX syntax:
// components/Counter.tsx (compiled with jsxImportSource: "preact")
export function Counter({ initial }: { initial: number }) {
return (
<div class="counter">
<button>-</button>
<span>{initial}</span>
<button>+</button>
</div>
);
}
// In your bext template
import { renderPreactComponent } from "@bext-stack/framework/preact";
import { Counter } from "./components/Counter";
const counterHtml = renderPreactComponent(Counter, { initial: 0 });
API
/** Render a Preact VNode to an HTML string. */
function renderPreact(vnode: VNode): string;
/** Render a Preact component with props to an HTML string. */
function renderPreactComponent(component: any, props?: Record<string, any>): string;
Solid adapter
The Solid adapter wraps Solid's renderToString for synchronous SSR.
Setup
bun add solid-js
Usage
import { renderSolid } from "@bext-stack/framework/solid";
function Greeting({ name }: { name: string }) {
return <h1>Hello, {name}!</h1>;
}
const html = renderSolid(() => <Greeting name="World" />);
// <h1>Hello, World!</h1>
API
/**
* Render a Solid component tree to an HTML string.
* Pass a function that returns the component (Solid's rendering convention).
*/
function renderSolid(fn: () => any): string;
Note: Solid is lazily loaded at runtime. If
solid-jsis not in your dependencies,renderSolid()throws a clear error message.
Mixing frameworks in one page
One of the key advantages of bext's architecture is that you can use different frameworks for different parts of the same page. The bext JSX layout renders the page shell, and React/Preact/Solid render individual components:
// server/template.tsx
import type { Page, RenderContext } from "@bext-stack/framework";
import { renderReact } from "@bext-stack/framework/react";
import { renderPreactComponent } from "@bext-stack/framework/preact";
import { DataTable } from "./components/DataTable"; // React component
import { SearchBox } from "./components/SearchBox"; // Preact component
export function renderPage(page: Page, ctx: RenderContext): string {
return (
<html lang="en">
<head>
<title>{page.title}</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
{/* bext JSX — zero overhead */}
<nav className="topnav">
<a href="/">Home</a>
<a href="/products">Products</a>
</nav>
<main>
{/* Preact component — 3KB, lightweight */}
<div dangerouslySetInnerHTML={{
__html: renderPreactComponent(SearchBox, {
placeholder: "Search products..."
})
}} />
{/* Page content */}
<div dangerouslySetInnerHTML={{ __html: page.html }} />
{/* React component — for complex data grids */}
<div dangerouslySetInnerHTML={{
__html: renderReact(DataTable, {
columns: ["Name", "Price", "Stock"],
endpoint: "/api/products"
})
}} />
</main>
{/* bext JSX footer — zero overhead */}
<footer>© 2026 My Store</footer>
</body>
</html>
);
}
When to use each
| Framework | Bundle size | Best for |
|---|---|---|
| bext JSX | 0 KB | Static layouts, navbars, footers, cards, page shells |
| Preact | ~3 KB | Lightweight interactive components, drop-in React replacement |
| React | ~140 KB | Complex components with hooks, existing React libraries |
| Solid | ~7 KB | Fine-grained reactive components, high-performance UIs |
General rule: Use bext JSX for everything that does not need client-side interactivity. Only reach for React/Preact/Solid when you have existing components or need their specific features for SSR.
Decision guide
- Static content (text, images, navigation) -- use bext JSX
- Server-rendered data display (tables, charts, lists) -- use Preact (small overhead) or React (if you already have the components)
- Interactive client components (forms, modals, real-time widgets) -- use the Islands pattern with React or Preact for client hydration
- Reactive data-driven UIs -- use Solid for fine-grained reactivity
These adapters are for server-side rendering only — they produce an HTML string that bext sends to the client. For interactive components that need to hydrate in the browser, pair them with the Islands pattern.
Performance notes
All three adapters use synchronous renderToString, which runs inside bext's render worker pool. Rendering is single-threaded per worker but parallelized across the pool (default: 4 workers).
The current canonical 38.3 KB byte-validated fixture reports:
| Approach | Render time |
|---|---|
bext JSX, direct h() |
69.3 µs average / 62.1 µs p50 |
| bext automatic JSX | 75.3 µs average / 67.9 µs p50 |
Solid renderToString |
78 µs average / 61 µs p50 |
React 19 renderToString |
1,823 µs average / 1,646 µs p50 |
Preact renderToString |
Not rerun in the current fixture |
These are rendering-only figures on Bun 1.3.13, pinned to one core—not full request latency. Preact's earlier range is intentionally omitted until it is rerun against the same DOM and byte-validation gate.
For pages that are mostly static with one or two embedded framework components, the overhead is negligible because bext's ISR cache stores the rendered HTML and serves subsequent requests from cache.
Related
- JSX runtime — bext's direct, safe-by-default built-in JSX, the default for page shells
- Islands pattern — client hydration for interactive React / Preact / Solid components
- PRISM — bext's native TSX SSR framework for full-page server rendering
- Client runtime — how bext ships JS to the browser
- Caching guide — ISR cache that stores the rendered HTML output