Communicating with Dart
Send messages between JS and Dart with channels
Channels are a simple messaging system for communication outside the widget tree. Use them for analytics, auth, native API calls, background tasks — anything that doesn't map to a widget or handle.
Three methods, symmetric on both sides:
send(channel, data)— fire-and-forget message.call(channel, data)— await the handler's return value.on(channel, handler)— register a handler. Its return value flows back to the caller ofcall.
You can import each individually, or the grouped channels object:
import { send, call, on } from "solid-fuse";
// or:
import { channels } from "solid-fuse";
channels.send("foo", { bar: 1 });Sending messages to Dart
import { send } from "solid-fuse";
send("analytics", { event: "page_view", page: "home" });On the Dart side, register a handler:
runtime.channels.on('analytics', (data) {
Analytics.track(data['event'], data['page']);
});Receiving messages from Dart
import { on } from "solid-fuse";
on("auth:token", (data) => {
setToken(data.token);
});The Dart side sends:
await runtime.channels.send('auth:token', {'token': jwt});Requesting a response from Dart
Use call when you need the handler's return value. The Dart handler can be sync or async; whatever it returns becomes the resolved Promise in JS. If the handler throws, the Promise rejects with the serialized error.
import { call } from "solid-fuse";
const result = await call("biometric/prompt", { reason: "unlock" });
if (result.verified) {
// ...
}runtime.channels.on('biometric/prompt', (data) async {
final ok = await Biometrics.authenticate(reason: data['reason'] as String);
return {'verified': ok};
});call has a default 30-second timeout. For long-running native prompts (biometrics, system dialogs), disable it with { timeout: 0 }:
const result = await call("biometric/prompt", { reason: "unlock" }, { timeout: 0 });The reverse direction — Dart calling JS and awaiting a response — works the same way. runtime.channels.call('channelName', data) returns a Future that completes with the JS handler's return value.
Choosing between send and call
send | call | |
|---|---|---|
| Returns | nothing (fire-and-forget) | awaits the handler's return value |
| Errors | silently dropped | propagate to the caller |
| Overhead | none beyond the FFI call | one Promise + one timer per call |
| Use for | events, notifications, logs | requests that need a response |
| Hot paths | yes | only if you need the value |
Rule of thumb: if you'd await the result, use call. Otherwise use send.
Complete example: auth flow
import { createSignal } from "solid-js";
import { call } from "solid-fuse";
const [token, setToken] = createSignal<string | null>(null);
const [error, setError] = createSignal<string | null>(null);
async function login(email: string, password: string) {
try {
const { token } = await call("auth:login", { email, password });
setToken(token);
} catch (e) {
setError(e.message);
}
}runtime.channels.on('auth:login', (data) async {
final token = await AuthService.login(
data['email'] as String,
data['password'] as String,
);
return {'token': token};
});Channels vs handles
| Channels | Handles | |
|---|---|---|
| Pattern | Request/response or fire-and-forget messages | Persistent object with state |
| Lifecycle | Global, no cleanup needed | Tied to component lifecycle |
| State | No built-in reactivity | Reactive signals (e.g. scrollOffset()) |
| Use for | Events, one-off requests | Ongoing imperative control |
Use channels when you need to send a message or make a request. Use handles when you need a persistent native object you can call methods on and read state from.
Reserved channels
Channel names starting with _ are reserved for Fuse internals (_ops, _functionCall, _ws, _wsEvent, _log). Use your own names for app channels.