bloom_js_native 0.3.7
bloom_js_native: ^0.3.7 copied to clipboard
Reactive Dart web framework for real DOM apps — SSR, SSG, ISR-friendly rendering, signals, SEO, and browser APIs. Not Flutter web.
bloom_js_native #
Bloom JS Native is the reactive web layer of Bloom: Dart components to real DOM, with SSR · SSG · ISR-friendly rendering, SEO, and browser-native APIs. It is not Flutter rendered in a browser.
Dart owns reactivity, compilation, and tooling; the browser owns rendering; npm is consumed surgically, never wholesale. Pair it with bloom_server for a full-stack Dart web application.
No Flutter on web. No VDOM. No hand-rolled package manager. Real DOM, real CSS, fine-grained signals.
One-liner mental model #
Dart component code
↓ builds
Descriptor tree (BloomNode: El / Text / Live / Fragment) ← pure Dart, VM-testable
↓ backend 1 ↓ backend 2
BrowserMount (package:web) renderToHtml() → String
real DOM + signal effects SSR / SSG / SEO / prerendering
Quickstart #
import 'package:bloom_js_native/bloom_js_native.dart';
void main() {
final count = signal(0);
final app = Fragment(children: [
H1(text: 'Counter'),
Live(() => P(text: 'Count: ${count.value}')), // reactive — closes over signals
Button(text: '+1', onClick: (_) => count.value++),
Show(() => count.value > 9,
child: P(text: 'Double digits!'),
fallback: P(text: 'Keep clicking')),
ForEach(() => todos.value, (t) => Li(children: [Text(t.title)])),
]);
mount(app, '#app'); // real DOM, effects auto-disposed on unmount
}
# Build (T0 — plain dart compile js)
dart compile js -O4 -o main.js main.dart
# or demo
cd example && bash build.sh
Comparison #
| JS concept | Bloom equivalent |
|---|---|
useState / zustand |
signal() / computed() / effect() (package:signals) |
useReducer |
BloomReducer / useReducer(reducerFn, initial) |
| React Context | createContext() / useContext() / BloomContext.provide() |
{expr} in JSX |
Live(() => P(text: '${count.value}')) |
{cond && <A/>} |
Show(() => cond, child: A) |
items.map(...) |
ForEach(() => items.value, (x) => ...) |
React Router loader/nested routes |
BloomRoute(loader:, dataBuilder:, layout:, guards:) |
| React.lazy + Suspense | lazy(loader, fallback:) (pairs with Dart deferred as) |
renderToPipeableStream |
renderToStreamWithSuspense(node) |
hydrateRoot |
hydrate(node, '#app') (true DOM-reuse for static trees; reactive trees fall back to a correct full remount) |
| React/Vite error overlay | renderDevErrorOverlay(), auto-shown via bloomDevErrorOverlayEnabled |
| React Testing Library | bloom_test — renderForTest() + fireEvent |
| React DevTools (inspector) | BloomJsDevTools.snapshotTree() / .eventLog |
| tanstack query | BloomQuery (native) / bloom_data (shared core) |
| tanstack mutation | BloomMutation (optimistic updates, rollback, invalidation) |
ng generate / CRA templates |
bloom js create <Name> [--page|--guard] |
| zod | bloom_validate / NpmDependency('zod', ...) bridge |
Honest npm compatibility statement #
Full arbitrary-npm compatibility is impossible without shipping
node_modules. Guarantee: any ESM-compatible, browser-safe package works via import maps (v0) / Bun vendor (v1). Anything needing Node globals, native addons, orwindowat import time needs a typed binding (v2) or thedart:js_interopescape hatch.
API #
- Elements:
Div,Span,P,H1-H4,Button,Input,A,Img,Ul/Ol/Li,Form,Header/Footer/Main/Nav/Section, plus genericEl('custom-tag', ...) - Props:
text,className,style,attrs: {k:v},on: {event: handler}, sugaronClick/onInput/onChange/onSubmit,children - Reactivity:
Live(() => ...),Show(() => bool, child:, fallback:),ForEach<T>(() => List<T>, (T) => BloomNode) - State management:
signal()/computed()/effect()/batch()(useState/useMemo/useEffect),BloomReducer/useReducer(useReducer),BloomController(Zustand-style store with lifecycle),createContext()/useContext()/BloomContext.provide()(Context) - Events: handlers receive
BloomEventwith.value,.checked,.preventDefault(),.stopPropagation()— VM-testable viaBloomEvent.fake*() - Mount:
mount(node, '#app')→BloomMountHandlewithunmount()/dispose();hydrate(node, '#app')for hydrating server-rendered markup — reuses existing DOM nodes in place (attaches listeners, patches text/attrs) for purely static subtrees, falls back to a safe full remount wherever the tree contains reactive nodes or the DOM doesn't structurally match - Lazy loading:
lazy(() async { ...; return Component(); }, fallback: ...)— Suspense-backed, pairs with Dart'sdeferred asfor real JS code-splitting (React.lazy equivalent) - SSR:
renderToHtml(node)→String(XSS-escaped, void elements handled);renderToStream(node)for simple chunked output;renderToStreamWithSuspense(node)for true out-of-order streaming SSR (ReactrenderToPipeableStreamequivalent) — flushes every Suspense fallback immediately (root, nested, or discovered inside resolved async content), streams resolved content as each boundary lands, independent of nesting depth - Data & mutations:
BloomQuery(cached, deduplicated, auto-revalidating fetches — tanstack query equivalent),BloomMutation(optimistic updates, rollback, cache invalidation) - Router:
BloomRouter+BloomRoute(nested layouts viaBloomRoute.shell,guards: [BloomRouteGuard],loader/dataBuilder/loadingFallbackfor React Routerloader-style data APIs — auto-revalidates viaBloomQuery+BloomMutation.invalidateKeys) +Link(href: ...) - Testing:
bloom_test—renderForTest(node)withgetByTestId/getByText/getByTagqueries andfireEvent.click/input/change/submit(Testing Library equivalent), operates on the descriptor tree with no browser required - DevTools:
BloomJsDevTools.snapshotTree(node)(serializable component tree),.eventLog/.notify()(bounded diagnostics event log) - Dev error overlay:
renderDevErrorOverlay(error, stackTrace)— full-screen HTML error overlay (React/Vite red-screen equivalent), wired intomount()'s error path viabloomDevErrorOverlayEnabled - npm:
NpmRegistry.register(NpmDependency('zod','^3.23.0'))→generateImportMapTag() - CLI:
bloom js dev/build/vendor, plusbloom js create <Name>(component),--page(route/page + BloomRoute snippet),--guard(BloomRouteGuard)
Styling #
Real DOM = real CSS:
- Plain
index.html<link>files - Tailwind via
className:(it's a real class attribute) - Scoped:
Style('a{color:red}')+ generated class names (phase 5 artifact) - Theme tokens: mirror
GEMINI.mdcarbon/indigo palette
Testing #
~90% VM-testable without a browser:
dart test # framework descriptors + renderToHtml goldens + npm + router
dart test -p chrome # mount/events against real DOM (phase M1 stretch)
Complete Documentation Suite #
- 01 — Thinking in Signals & Pure Dart AST
- 02 — Describing the UI (Elements, Fragments & Keyed Lists)
- 03 — Reactivity & State Deep Dive (Signals, Computed, Batching)
- 04 — Interactivity, Events & Forms
- 05 — Server-Side Rendering (SSR) & Static Generation (SSG)
- 06 — NPM Ecosystem & JavaScript Interop
- 07 — Developer Tooling & CLI Suite (Zero-Python Dev Server)
- 08 — Complete API Reference
- 09 — Testing, DevTools, Lazy Loading & Resilience
- Known Issues & Tracked Work
Status #
Core rendering engine (SSR/SSG, streaming SSR, hydration), fine-grained
signals-based reactivity (state, reducer, context, controller stores),
routing (nested layouts, guards, data loaders with revalidation),
component testing utilities, lazy loading, a DevTools inspector, a dev
error overlay, and CLI scaffolding (bloom js create) are implemented.
Hydration performs true DOM-reuse (React hydrateRoot-style: walks
existing server-rendered nodes in place, attaches listeners without
recreating them) for purely static trees, and safely falls back to a
full remount for any tree containing a reactive node or a structural
mismatch against the actual DOM — verified via static analysis and a
successful dart compile js of the browser entrypoint; this repo has
no headless-browser runner available to exercise it against a live
DOM, so treat it as reviewed-but-not-runtime-verified until it's been
run in an actual browser. Progressive streaming covers Suspense
boundaries at any nesting depth, including boundaries discovered inside
another boundary's resolved content.
See root GEMINI.md § Bloom JS Native.