Rask
Rask
v0.23.1-alpha.0.496
You're reading the Rask v0.23.1-alpha.0.496 guides. Updated .View source
All guides

Migrating from Blazor

Rask isn't a Blazor replacement so much as a different take on the same problem space. If you know Blazor, most concepts transfer directly — they just move from .razor markup into plain C#. This guide maps the day-to-day APIs and calls out the behavioural differences that trip people up.

New to Rask entirely? Start with getting started.

Concept mapping

Blazor Rask
.razor component (markup + @code) sealed partial class : Component with Render() returning a tree
RenderFragment / Razor markup A chain — Div.Class("panel")[Span["hi"]], children via an indexer
@onclick="Handler" .OnClick(() => ...) (a lambda, set by a chain step)
[Parameter] public T X { get; set; } public T X { get; set; } — becomes a chain step (required if non-nullable) or an optional setter
EventCallback / EventCallback<T> Callback / Callback<T> — one property taking a sync (Action/Action<T>) or async (Func<Task>/Func<T,Task>, or a ValueTask) handler
[CascadingParameter] / CascadingValue Context.Provide(value) / Context.Get<T>() / Context.Required<T>()
@key="x" .Key(x) — an ordinary chain step
OnInitialized / OnInitializedAsync OnMount() — one hook; the part before its first await is the synchronous half
OnParametersSet / OnParametersSetAsync OnUpdated()
OnAfterRender(firstRender) / async OnFirstRender() for the firstRender branch, OnRendered() for every render
Dispose / DisposeAsync implement IDisposable / IAsyncDisposable; or use OnUnmount()
NavigationManager Go (event-handler-only) + RouteState (current path/params)
@page "/path" [Route("/path")] on the class — Rask's Route, from Rask.Core.Routing; Blazor's attribute of the same name leaves the page unregistered (RASK071)
route/query binding [RouteParam] / [QueryParam] on a property
<EditForm> + InputText/InputNumber Form.Model(model).OnSubmit(…) + Input.Bind(() => model.X)
InputDate / InputSelect the kit's Ui.DatePicker.Bind(() => model.Day) / Ui.Select.Bind(() => model.Plan).Listbox[…] (UI kit)
@bind (writes on change) / @bind:event="oninput" .Bind(…) writes with the next action; .Blur() on leaving the field; .Live() as it is typed
<DataAnnotationsValidator> nothing — a Form validates its model itself
<AuthorizeView> (+ Context="user" / @context.User) headless Authorize — its .Authorized(user => …) slot receives the ClaimsPrincipal, like @context.User
AuthenticationStateProvider inject IUserProvider and read .Current
[Inject] T Svc { get; set; } constructor injection — partial class Page(T svc) : Component
Component.razor.css sibling {Component}.css next to {Component}.cs
IJSRuntime IJSRuntime (injected via the constructor)

Side-by-side examples

A parameterised component

@* Blazor: Greeting.razor *@
<h1>Hello, @Name!</h1>
@code {
    [Parameter] public string Name { get; set; } = "";
}
// Rask
public sealed partial class Greeting : Component
{
    public required string Name { get; set; }   // non-nullable → a required chain step
    protected override Component? Render() => H1[$"Hello, {Name}!"];
}
// call site: Greeting.Name("Ada")

Event handler + local state

@* Blazor *@
<button @onclick="() => count++">Count: @count</button>
@code { int count; }
// Rask — owner re-renders automatically after the handler
Button.OnClick(() => _count++)[$"Count: {_count}"]

Component → parent callback (no EventCallback)

This is the biggest API difference. Blazor wraps child events in EventCallback so the parent re-renders. Rask's child declares a Callback<T> prop instead — a struct that takes either a sync or an async handler, with no InvokeAsync to thread through — and the chain step that sets it wraps it so invoking it re-renders the parent that owns the lambda (the lambda's this).

@* Blazor: child *@
<button @onclick="() => OnRate.InvokeAsync(5)">Rate</button>
@code { [Parameter] public EventCallback<int> OnRate { get; set; } }
// Rask: child declares a Callback<int>; parent passes a lambda over its own state
public sealed partial class RatingStars : Component
{
    public Callback<int> OnRate { get; set; }
    protected override Component? Render() =>
        Button.OnClick(() => OnRate.Invoke(5))["Rate"];
}

// parent — the lambda captures `this`, so invoking OnRate re-renders the parent
RatingStars.OnRate(n => _rating = n)

A static method, or a lambda closing over a local instead of this, has no component target, so no auto re-render fires — write the lambda inside the component that should update.

Cascading value → Context

@* Blazor *@
<CascadingValue Value="theme"><Component /></CascadingValue>
@* in Component: *@ [CascadingParameter] Theme Theme { get; set; }
// Rask — provide high, read deep (nearest provider wins, matched by type)
Context.Provide(_theme)[ ThemeCard ]
// in any descendant's Render():
var theme = Context.Required<Theme>();   // or Context.Get<T>() (null if absent)

Routing + navigation

@* Blazor *@
@page "/users/{Id:int}"
@inject NavigationManager Nav
@code { [Parameter] public int Id { get; set; } }
// Rask
[Route("/users/{id}")]
public sealed partial class UserPage : Component                   // nothing injected to navigate
{
    [RouteParam] public int Id { get; set; }
    [QueryParam] public string? Tab { get; set; }
    // navigate from a handler: Routes.HomePage().Go(), Go.With("tab", "x")
}

// type-safe link (generated URL builder) instead of a "/users/42" string:
NavLink.Href(Routes.UserPage(42))["View user"]

Forms

@* Blazor *@
<EditForm Model="model" OnSubmit="Save">
    <DataAnnotationsValidator />
    <InputText @bind-Value="model.Name" />
</EditForm>
// Rask
Form.Model(_model).OnSubmit(m => Save(m))[
    // no validator declared -- the attributes on _model are enforced by the Form itself
    Input.Bind(() => _model.Name),              // input type inferred from the CLR type
    Validation.Message.Template(errs => Div.Class("field-error")[errs[0]]).For(() => _model.Name),
    Button.Type(ButtonType.Submit)["Sign up"]
]

Input/Select/Textarea infer their type from the bound property (string → text, bool → checkbox, int → number, DateOnly → date). Validation.Message and Validation.Summary are headless — you chain a .Template(…) lambda for the markup. See forms for nested models, collections, and async validation.

The attributes you already have keep working. A new rule is usually written beside its field instead — Input.Bind(() => _model.Name).Validate(v => v.Length > 0 ? [] : ["Name is required."]) — see forms — validation.

Scoped CSS

Identical idea, no association ceremony. Drop a sibling {Component}.css next to {Component}.cs and it's auto-globbed, scoped to that type, and hot-reloaded under dotnet watch — Blazor-parity descendant scoping. There is no per-selector opt-out (no :global(...), no ::deep): global styles for shell tags like body/html, brand palettes, or framework classes go in a plain wwwroot stylesheet linked from your App's <Head>.

Behavioural gotchas

  • Children come through an indexer, not a Children parameter. Write Div[Span["hi"], "there"]. There is no ChildContent step, and Children is always excluded from the generated chain.

  • The .. spread fails inside [...]. The C# spread element parses as a Range inside the indexer. Pass enumerables directly instead:

    Ul[items.Select(i => Li.Key(i.Id)[i.Name])]              // ✓ pass the sequence
    // Ul[.. items.Select(...)]                            // ✗ parses as Range
  • F12 / Go-to-Definition on a chain step lands on the generated member, not your component class. Stock Roslyn/ReSharper navigate a generated symbol to its generated document. Use the IDE's "Navigate to Type of Symbol" for a one-action jump to the component class. (Every generated member carries a <see cref> breadcrumb and [DebuggerStepThrough], so hover/Quick-Doc offers a navigable link and the debugger steps over the generated code into yours.)

  • Keyed lists matter — and Rask warns when you forget. Like Blazor's @key, pass .Key(…) on list items so inserts/removes/reorders reconcile by identity (preserving focus and input state). A Rask chain in a list-projection context (.Select/.SelectMany, or .Add in a loop) without a .Key(…) raises RASK022 — keyless items reconcile positionally.

  • Go is event-handler-only. Calling it outside a handler throws. Read the current route from RouteState; change it with Go from a handler.

  • Lifecycle re-fire rules differ. Updated* fires on first render and whenever a bound prop or route/query param actually changes — a bare event-handler re-render does not re-fire it. Don't call StateHasChanged() in Unmount*. Faulted async hooks log to Console.Error and don't re-render (a placeholder that never resolves usually means an async hook threw).

What stays the same

  • DI through the constructor — exactly like the rest of .NET. Framework services (RouteState, HttpClient, IJSRuntime) inject the same way as your own services. (No [Inject] properties — a non-nullable settable property would become a required chain step instead.)
  • Scoped CSS parity — sibling {Component}.css, descendant-combinator scoping, hot reload.
  • IJSRuntime — the same interop surface (InvokeAsync, InvokeVoidAsync). Rask adds element refs (new ElementRef<HTMLInputElement>() + a .Ref(…) step on every element, carrying MDN's members: await _name.Focus()) and a sibling {Component}.ts convention bundled and dispatched as window.Rask["{TypeName}"].

Confirm