solid-fuse

Architecture

How JSX becomes Flutter widgets

Fuse connects SolidJS and Flutter through a surprisingly small set of ideas. Each layer does one thing well, and they compose naturally. This page walks through how it all fits together.

Two threads, one reactive loop

SolidJS runs inside FJS — a QuickJS-based JavaScript runtime built in Rust. FJS runs on its own isolate thread, off Flutter's main thread, so the JS engine and Flutter's frame pipeline never compete for the same core.

Components create a virtual tree of nodes using @solidjs/universal (the same renderer API behind Solid's SSR). Each node has an ID, type, props, and children. When a signal updates, Solid records the changes as operations — create a node, set a prop, insert a child — and batches them into a single list. Ten prop changes become one bridge call, not ten.

The batch crosses to Dart through FJS's Rust bridge as a single FFI call. FuseRuntime applies the ops, and each node is a ChangeNotifier. Flutter's own ListenableBuilder picks up the changes and rebuilds the affected widgets — no custom diffing or reconciler needed.

Events flow back the same way. A GestureDetector fires, Dart calls into JS with the node ID and event name, the signal updates, new ops flush to Dart. The whole loop runs automatically.

signal update → ops journal → bridge_call → Dart applies ops → Flutter rebuilds

signal update ← JS handler  ← bridge_call ← Flutter gesture ← user taps

The runtime

Most hybrid frameworks run JS in a stripped-down environment where IO operations bounce back to the host. FJS implements standard APIs — fetch, crypto, setTimeout, Buffer, URL parsing, streams, compression — in Rust. So things like network requests and cryptography run at native speed, not through JS polyfills. SolidJS gets a real environment with the APIs it expects.

Fuse adds a WebSocket polyfill on the Dart side (via web_socket_channel) to fill that gap.

For production, bundles can be pre-compiled to QuickJS bytecode — no parse step at launch, just load and run.

The bridge

Only things that need Flutter cross the bridge: rendering ops, gesture events, platform APIs. Reactive state, data fetching, timers, module imports — all of that stays inside the Rust/QuickJS layer on its own thread.

There's no JSON serialization. FJS passes structured values directly between Dart and JS through Rust. On the Dart side, FuseNode and FuseMap provide typed accessors (node.string('label'), node.color('color')) that read these values directly.

Extending

Extending Fuse only requires Dart — no Rust, no C. Write a Dart class, add a JSX type declaration, register it. Any Flutter developer already knows enough. And because the Dart side is ordinary Flutter, any pub.dev package can be wrapped and driven from JSX — see Using pub.dev packages.

Two registration types:

MethodPurposeCreates
registerWidget(name, builder)Visual elementsStatelessWidget / StatefulWidget
registerHandle(name, factory)Imperative control and navigator pagesFuseHandle<T> wrapping a persistent native object (controllers, focus nodes, Flutter Page objects via FusePageHandle)

Packages

Fuse packages live on npm. Each package can include both JS and Dart code. The CLI (fuse link) scans node_modules for Fuse packages, reads their Dart source, and generates the glue code to wire them into your Flutter project.

bun add some-fuse-package
fuse link
# Dart dependencies resolved, register function generated

One package manager for both sides.

Dev vs production

Developmentfuse dev starts Vite and Flutter together. Dart connects to the Vite dev server via WebSocket, pre-fetches ES modules, and evaluates them in FJS. HMR works — edit, save, see it on device.

Productionfuse build produces a single IIFE bundle with optional bytecode compilation. The Dart layer is a stable native SDK that rarely changes. The JS bundle is where product code lives and can ship continuously — including over-the-air without app store review.

On this page