Islands on WebAssembly
An island is an ordinary Rask component whose markup a front-end framework produces. These are the same files the Server showcase builds — C# owns the props, the generated types cross back into the .vue, the two .tsx and the .svelte, and the subtree is a diff boundary Rask never patches into. Only the transport differs.
Four runtimes in one tree, running client-side. The callback that reaches C# here does so through a [JSExport] call into this tab's own runtime; the front-end files are byte-identical to the Server showcase's.
using Rask.Site;
namespace Rask.Site.Features.Islands;
/// <summary>
/// The same islands the Server showcase runs, on the WASM host — from byte-identical front-end
/// files.
/// </summary>
/// <remarks>
/// <para>
/// Nothing in <c>VueChart.vue</c>, <c>ReactCounter.tsx</c> or <c>SvelteMeter.svelte</c> knows
/// which host it is on. On Server a callback rides the live WebSocket; here it is a
/// <c>[JSExport]</c> call straight into this tab's own runtime. The island never opens a
/// channel of its own either way, so it inherits sequence stamping and the
/// queue-while-reconnecting for free.
/// </para>
/// <para>
/// Lit and Angular are absent here, and that is deliberate rather than an omission: both pair
/// with a plain <c>.ts</c>, which collides with Rask's scoped TypeScript — and this app
/// genuinely uses it. The Server showcase, which has no scoped TypeScript at all, carries both.
/// </para>
/// <para>
/// React and Solid sit in folders of their own because they both compile <c>.tsx</c>, so their
/// Vite plugins are each scoped to their own island directory. Sharing one — or nesting one
/// inside the other — is refused by the build rather than mis-compiled.
/// </para>
/// </remarks>
public sealed partial class IslandsDemo : Component
{
private readonly List<ChartBar> _series =
[
new("Jan", 38),
new("Feb", 64),
new("Mar", 51),
new("Apr", 82),
];
private readonly List<int> _readings = [12, 30, 22, 48, 35, 61];
private int _reading = 40;
private int _lastClicked;
private int _clicks;
private int _step = 1;
private int _reactTotal;
private int _hoveredPoint = -1;
protected override Component? Render() =>
[
Div.Class($"{Tw.Card} shadow-sm border-0 mb-3")[
Div.Class(Tw.CardBody)[
H6.Class("font-bold")["A Vue island calling back into C#, in WebAssembly"],
P.Class("text-sm text-ui-muted")[
"The same ", Code["VueChart.vue"], " the Server showcase builds. Clicking a bar ",
"re-enters C# — here through a ", Code["[JSExport]"], " call into this tab's ",
"runtime rather than over a socket."
],
VueChart.Series(_series).Heading("Revenue by month").OnBarClick(BarClicked),
P.Class("text-sm mt-3 mb-0")[
"Last bar clicked: ",
Code.Id("island-last-clicked")[_lastClicked == 0 ? "(none)" : _lastClicked.ToString()],
Span.Class("ms-2")["after "],
Code.Id("island-clicks")[_clicks.ToString()],
Span[" click(s)"]
]
]
],
Div.Class($"{Tw.Card} shadow-sm border-0 mb-3")[
Div.Class(Tw.CardBody)[
H6.Class("font-bold")["React and Svelte keeping their own state"],
P.Class("text-sm text-ui-muted")[
"Both hold state C# never sees. Raising the reading re-renders this component, and ",
"the counters below have to survive it — a remount would reset them, and nothing ",
"else on the page would look any different."
],
ReactCounter.Caption("Clicks since mount").Step(_step).OnTotalChanged(TotalChanged),
Div.Class("mt-3")[
SvelteMeter.Value(_reading).Label("Capacity")
],
Div.Class("flex gap-2 mt-3")[
Button.Class(Tw.BtnPrimary).Id("island-raise").OnClick(Raise)["Raise the reading"],
Button.Class(Tw.BtnOutlinePrimary).Id("island-reset").OnClick(Reset)["Reset"]
],
P.Class("text-sm mt-3 mb-0")[
"React reported a total of ",
Code.Id("island-react-total")[_reactTotal.ToString()],
Span[" back to C#."]
]
]
],
Div.Class($"{Tw.Card} shadow-sm border-0 mb-3")[
Div.Class(Tw.CardBody)[
H6.Class("font-bold")["A Solid island, from the same file the Server showcase builds"],
P.Class("text-sm text-ui-muted")[
"Byte-identical to ", Code["SolidSpark.tsx"], " on the Server host. Its hover count ",
"belongs to Solid and C# never sees it, so raising the reading has to reach it as a ",
"prop change rather than a remount."
],
SolidSpark.Readings(_readings).Caption("Throughput").OnPointHovered(PointHovered),
P.Class("text-sm mt-3 mb-0")[
"Last point hovered: ",
Code.Id("island-hovered")[_hoveredPoint < 0 ? "(none)" : _hoveredPoint.ToString()]
]
]
]
];
private void BarClicked(int value)
{
_lastClicked = value;
_clicks++;
}
private void TotalChanged(int total) => _reactTotal = total;
private void PointHovered(int index) => _hoveredPoint = index;
private void Raise()
{
_reading = Math.Min(100, _reading + 15);
_step++;
for (var i = 0; i < _series.Count; i++)
{
_series[i] = _series[i] with { Value = Math.Min(100, _series[i].Value + 4) };
}
for (var i = 0; i < _readings.Count; i++)
{
_readings[i] = Math.Min(100, _readings[i] + 5);
}
}
private void Reset()
{
_reading = 40;
_lastClicked = 0;
_clicks = 0;
_step = 1;
_reactTotal = 0;
_hoveredPoint = -1;
_readings.Clear();
_readings.AddRange([12, 30, 22, 48, 35, 61]);
_series.Clear();
_series.AddRange([new("Jan", 38), new("Feb", 64), new("Mar", 51), new("Apr", 82)]);
}
}
A Vue island calling back into C#, in WebAssembly
The same VueChart.vue the Server showcase builds. Clicking a bar re-enters C# — here through a [JSExport] call into this tab's runtime rather than over a socket.
Last bar clicked: (none)after 0 click(s)
React and Svelte keeping their own state
Both hold state C# never sees. Raising the reading re-renders this component, and the counters below have to survive it — a remount would reset them, and nothing else on the page would look any different.
React reported a total of 0 back to C#.
A Solid island, from the same file the Server showcase builds
Byte-identical to SolidSpark.tsx on the Server host. Its hover count belongs to Solid and C# never sees it, so raising the reading has to reach it as a prop change rather than a remount.
Last point hovered: (none)