solid-fuse

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 of call.

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

sendcall
Returnsnothing (fire-and-forget)awaits the handler's return value
Errorssilently droppedpropagate to the caller
Overheadnone beyond the FFI callone Promise + one timer per call
Use forevents, notifications, logsrequests that need a response
Hot pathsyesonly if you need the value

Rule of thumb: if you'd await the result, use call. Otherwise use send.

Complete example: auth flow

JS side
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);
  }
}
Dart side
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

ChannelsHandles
PatternRequest/response or fire-and-forget messagesPersistent object with state
LifecycleGlobal, no cleanup neededTied to component lifecycle
StateNo built-in reactivityReactive signals (e.g. scrollOffset())
Use forEvents, one-off requestsOngoing 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.

On this page