# Best practices

You've read [getting started](https://rask.sh/docs/guides/getting-started/) and shipped a component or two. This page
collects the patterns that keep a Rask app correct, secure, and fast as it grows — the rules the
framework rewards, the foot-guns it can't stop for you, and where each one is enforced.

Every item is short on purpose: a rule, *why* it matters, and a link to the deep dive. Many of these
are also compile-time diagnostics ([RASK001–034](https://rask.sh/docs/guides/diagnostics/)) — when the analyzer can catch a
mistake, the rule notes the ID.

- [Component design](#component-design)
- [Rendering, keys & encoding](#rendering-keys--encoding)
- [State, callbacks & events](#state-callbacks--events)
- [Context & dependency injection](#context--dependency-injection)
- [Forms & validation](#forms--validation)
- [Routing & lifecycle](#routing--lifecycle)
- [Data access & side effects](#data-access--side-effects)
- [JavaScript interop & refs](#javascript-interop--refs)
- [Security](#security)
- [Accessibility](#accessibility)
- [Performance & memory](#performance--memory)
- [Testing](#testing)
- [Common pitfalls](#common-pitfalls)

---

## Component design

- **Make every component `sealed partial`.** Components aren't an inheritance hierarchy; sealing states
  intent and keeps the generator's analysis simple, and `partial` is where the generator puts the chain
  surface (**RASK036** without it). Every sample does this.
- **Build components with the chain, never `new`.** `Div.Class("panel")`, `Counter`,
  `RatingStars.Value(3)` — the chain wires keys, children, callbacks, and DI that `new` skips.
  Outside `Rask.Core`, `new`-ing a component is **RASK014**. (Test files that define their own
  `Component` subclasses opt out with `#pragma warning disable RASK014`.)
- **Inject framework services through the constructor, not as properties.** A non-nullable settable
  property becomes a *required step* — so `public IJSRuntime Js { get; set; }` would
  force callers to pass it. Take services as ctor parameters instead:
  ```csharp
  public sealed partial class Weather(IWeatherForecastService service) : Component { /* ... */ }
  ```
  Combining a `required` property with a DI constructor is contradictory — that's **RASK002**.
- **Know what becomes a step.** The generator derives the chain from your public
  settable properties: non-nullable + no initializer → a **required step** (a hidden **RASK001** suggests
  marking it `required` so it reads as intentional); nullable → an optional setter; an
  initializer (`= ...`) → an optional setter; `[SkipFactory]` or `Children` → excluded. Reach for
  `[SkipFactory]` to keep internal state off the chain entirely. See
  [getting started §6](https://rask.sh/docs/guides/getting-started/#6-why-homepage-already-chains-the-generated-surface).

## Rendering, keys & encoding

- **Give every list item a stable, unique `.Key(…)`.** Keys are the reconciliation identity the live
  diff uses to *move* a row instead of rebuilding it — preserving focus, input value, and scroll on
  reorder. A keyless list item is **RASK022**; duplicate sibling keys make the diff fall back to a
  positional walk (and report a one-time `data-rask-key` error via the diagnostics seam — treat it as
  a bug). Use
  entity IDs, not loop indices. See [composition](https://rask.sh/docs/guides/composition/#children--fragments).
- **Trust `Text` for anything user-supplied; reserve `Raw` for markup you control.** A plain string
  becomes a `Text` node and is **HTML-encoded**; `Raw(...)` emits verbatim. User input through `Raw`
  is an XSS hole. See [getting started → your first component](https://rask.sh/docs/guides/getting-started/#4-your-first-component).
- **Leave the page shell to the framework.** The `TApp` root renders into `<body>`; Rask emits the
  doctype, `<html>`, `<head>` and `<body>` around it (the runtime `<script>` is appended to `<body>`,
  `<head>` is filled from each component's `HeadAssets` override). Rendering the shell yourself is
  **RASK021** — a second document nested inside the body, which the HTML parser silently unwraps. Set
  `<html lang>` with the `HtmlLang` override and `<body class>` with `BodyClass`; for anything else,
  override `Shell(head, body)` and place both parameters.
- **Contribute to `<head>` via the `HeadAssets` override, not `Head[…]` children.** `Head` is a managed
  slot; passing it children is **RASK019**. Override `protected override Component? HeadAssets` instead;
  `<title>`/`<base>` are singletons where the last contributor wins. See
  [getting started §7](https://rask.sh/docs/guides/getting-started/#7-the-document-and-the-headassets-override).
- **Don't fight the attribute order.** Universal attributes always render
  `id, class, style, data-*, role, tabindex, aria-*` (typed, then the Aria bag), then tag-specific. Tests assert it and it's
  stable across releases — match it when asserting on HTML.

## State, callbacks & events

- **Raise child→parent events with a `Callback<T>` prop.** Declare it non-nullable
  (`public Callback<int> OnRate { get; set; }`) and fire it with `await OnRate.Invoke(n)` — unset is a
  no-op. The parent passes any handler shape, sync or async; the chain step wraps it so invoking it
  re-renders the parent that owns the lambda, with no `StateHasChanged` by hand. Write the lambda
  *inside* the component so it captures `this`:
  ```csharp
  RatingStars.Value(_rating).OnRate(n => _rating = n)   // lambda captures this → parent re-renders
  ```
  A lambda over a plain local or a static method isn't wrapped and won't trigger a re-render. See
  [composition → callbacks](https://rask.sh/docs/guides/composition-callbacks-context/#callbacks-child--parent).
- **Don't expect a handler-only re-render to refire `OnUpdated`.** Auto-wrapped handlers are
  excluded from the `propsChanged` diff — changing only the lambda's identity doesn't refire it.
  `Updated*` fires when a *bound* value (a prop, a route/query param) actually changes. See
  [lifecycle → when Updated refires](https://rask.sh/docs/guides/lifecycle/#when-onupdated-refires).
- **Let the runtime re-render for you; call `StateHasChanged()` only for out-of-band state.** An
  awaited event handler and each `await` in an async lifecycle hook auto-re-render. You only call
  `StateHasChanged()` by hand for state that changes *outside* the handler-dispatch window — a timer
  tick, a fire-and-forget continuation, or an external event/observable you subscribed to in
  `OnMount`.
- **Thread `CancellationToken` into the async work a handler or hook starts.** The token cancels on
  unmount and — while a handler runs — when the host cancels that dispatch (the server's
  `HandlerTimeout` or a closed socket). Without it, slow work pins the session's render pipeline:
  ```csharp
  Button.OnClick(async () => _rows = await _api.Load(CancellationToken))["Load"]
  ```
  See [composition → cancelling async work](https://rask.sh/docs/guides/composition-callbacks-context/#callbacks-child--parent) and
  [lifecycle → cancellation](https://rask.sh/docs/guides/lifecycle/#cancellation-tied-to-component-lifetime).

## Context & dependency injection

- **Use `Context` to skip prop drilling, not as a general data bus.** `Context.Provide(value)`
  near the top, then `Context.Get<T>()` / `Required<T>()` / `Has<T>()` *inside `Render()`* below.
  Reading a context value latches the consumer out of the render cache, so it stays reactive even
  through a render-cached intermediate — that's the point. Provide a concrete type and consume by an
  interface if you like. See [composition → context](https://rask.sh/docs/guides/composition-callbacks-context/#context-provide--consume).
- **Always pair a manual subscription with its teardown.** If a component *above* the `Router` (a
  sidebar, breadcrumb) needs to react to navigation or a store, subscribe in `OnMount` and
  unsubscribe in `OnUnmount` — otherwise the publisher keeps a strong reference to the unmounted
  component:
  ```csharp
  protected override async Task OnMount()   => route.Changed += StateHasChanged;
  protected override async Task OnUnmount() => route.Changed -= StateHasChanged;
  ```
- **A prop that is a mutable collection does not re-render the child when you append to it.** Props
  are compared with `EqualityComparer<T>.Default`, which for a `List<T>` is reference equality — so a
  parent that appends to a list it owns and calls `StateHasChanged()` re-renders *itself*, while the
  child holding that same list is served from the render cache and never sees the new entries:
  ```csharp
  // the parent appends to _log and re-renders; LogView shows the OLD contents forever
  LogView.Entries(_log)
  ```
  Three ways out, in order of preference: hand the child a fresh snapshot (`_log.ToArray()`) so the
  reference genuinely changes; give the child something to subscribe to; or, when the child plainly
  reads state it does not own and there is no event to subscribe to, opt it out with
  `protected override bool BypassRenderCache => true;`. The same rule is what makes `Router` and
  `Outlet` opt out — they publish per-frame route state the rest of the walk depends on.

## Forms & validation

- **Bind two-way with a `Bind` expression.** `Input.Bind(() => _model.Name)` replaces `Value` +
  `OnInput`/`OnChange` + parsing, and infers the input type from the property's CLR type. A bound
  control writes the model with the next action; say `.Live()` on one you read back while the reader is
  still typing ([bind timing](https://rask.sh/docs/guides/forms/#bind-timing)). It also *replaces* them: a bound control
  installs its own write-back, so `Value`/`Checked`/`OnInput`/`OnChange` are not offered on a bound
  chain (and `AfterBind` is not offered on a controlled one) — reach for `AfterBind` when you want a
  side effect on each bound write. See [forms §1](https://rask.sh/docs/guides/forms/#1-two-way-binding).
- **Write the rule beside the field.** `Input.Bind(() => _model.Email).Validate(v => …)` returns the
  messages that reject the value; a rule across fields goes on `Form.Model(m).Validate(m => …)`, and
  either takes an `async` rule. The form owns the `EditContext` (touched/modified state + the validator
  pipeline). A model that already carries DataAnnotations attributes or has an `AbstractValidator<T>`
  is validated by those too, across the whole reachable object graph, with nothing declared. See
  [forms — validation](https://rask.sh/docs/guides/forms-validation/).
- **Bind collections with `foreach` + per-item capture** — the canonical pattern. Each iteration
  closes over a distinct instance, so each row owns its validation state and `foreach` has no closure
  trap:
  ```csharp
  foreach (var item in _model.Items)
      rows.Add(Tr[Td[Input.Bind(() => item.Description)]]);
  ```
  Only reach for the indexer style (`() => _model.Items[i].Name`) when you need the row number or
  replace records rather than mutate them — and then copy the loop index into a per-iteration local.
  See [forms §7](https://rask.sh/docs/guides/forms-advanced/#nested--complex-models).
- **Keep a field's simple rules in a value object.** Required, length and format belong to the value:
  expose them as a `static IEnumerable<string> Validate(T value)` (the shape of an inline rule) and
  name it from the field — `Input.Bind(() => _form.Price).Validate(Money.Validate)` — *and* enforce it
  inside the aggregate. One source of truth. See
  [rules in a value object](https://rask.sh/docs/guides/forms-validation/#rules-in-a-value-object).

## Routing & lifecycle

- **`[Route]` registers a page; bind URL pieces with `[RouteParam]` / `[QueryParam]`.** Path
  segments use `[RouteParam]`, query keys use `[QueryParam]` — swapping them is **RASK006**. Bound
  types must be `string` or `IParsable<T>` (**RASK011** otherwise), and must match a route constraint
  (`{id:int}`) when present. What the URL does not carry keeps the property's initializer —
  `[QueryParam] public int Page { get; set; } = 1;`, and an optional segment left out
  (`{count?}` with `[RouteParam] public int Count { get; set; } = 5;`) alike — and a query value the property
  cannot hold (`?page=two`) is read as absent, never an error. Link with the generated, refactor-proof
  `Routes.Page(...)` builder, not hand-written paths: it writes only the query arguments it is given, so
  `Routes.OrdersPage()` is the clean `/orders` and `Routes.OrdersPage(Page: 3)` is `/orders?Page=3` — do not
  make a query property nullable just to keep its key out of a link. See [routing](https://rask.sh/docs/guides/routing/).
- **Navigate from event handlers only.** `Go.To`, `Go.With` and a route's `.Go()` throw if called during `Render()`
  — it would mid-render the page out from under itself. Load-time redirects belong in `OnMount` or a route
  guard, not `Render()`. See [routing → Go](https://rask.sh/docs/guides/routing/#programmatic-navigation--go).
- **Put the right work in the right hook.** `OnMount` for a one-time load; `OnUpdated`
  to reload when a route/query param changes; `OnFirstRender` / `OnRendered` for post-paint side effects (it's
  loop-safe — a re-render elsewhere won't refire it). Each `await` auto-re-renders, so mutate state
  after the await and it paints. See [lifecycle](https://rask.sh/docs/guides/lifecycle/).
- **A faulted hook takes the page** — an `OnMount`/`OnUpdated` that throws, before or after its first
  `await`, shows the built-in error page in place of the whole page (a first request answers 500). Wrap
  risky hook work in `try/catch` to render your own error state, or put an `ErrorBoundary` around it. See
  [lifecycle → gotchas](https://rask.sh/docs/guides/lifecycle/#gotcha-a-faulted-async-hook-takes-the-page-not-the-component).
- **Never `StateHasChanged()` in unmount** — the component is already leaving the tree, so it's a
  no-op by design. Use `OnUnmount` to tear down subscriptions, nothing more. Compose nested layouts
  with `[ParentRoute]` + `Outlet`.

## Data access & side effects

- **Register `IDbContextFactory<T>`, not a scoped `DbContext`.** A Server session is long-lived; a
  `DbContext` is not thread-safe and is meant to be short-lived. Open a fresh context per unit of
  work and dispose it:
  ```csharp
  await using var db = await dbContextFactory.CreateDbContextAsync(CancellationToken);
  var products = await db.Products.AsNoTracking().ToListAsync(CancellationToken);
  ```
  Thread `Component.CancellationToken` into every async EF call so navigating away cancels in flight.
- **Load in a lifecycle hook, store in a field, render the field** — never query in `Render()`
  (which runs on every event the page hears). For an event-handler mutation, do the work and reload; the awaited
  handler re-renders on completion automatically. See [data access](https://rask.sh/docs/guides/data-access/).
- **Keep EF Core on the Server.** The SQLite provider isn't a fit for the trimmed WASM runtime — a
  WASM app should reach data through an API. A [`decimal` is safe to use](https://rask.sh/docs/guides/data-access/#does-sqlite-support-decimal):
  `UseRaskSqlite` fixes the upstream collation bug that otherwise mis-sorts it on `de-DE` and crashes
  the process on `en-HU`. Model money as integer minor units when the table is large and often sorted,
  for the native `INTEGER` index — not to dodge a correctness problem.

## JavaScript interop & refs

- **Type an element ref to its MDN interface, in a field.** `new ElementRef<HTMLDialogElement>()` keeps its id
  stable across renders (a local resets each render) and carries the element's MDN members:
  `await _dialog.ShowModal()`, `await _name.Focus()`. See [JS interop → element refs](https://rask.sh/docs/guides/js-interop-runtime/#element-refs).
- **Inject `IJSRuntime` through the constructor and call from a hook or handler** — interop is only
  live once the session is up (after `OnMount`, or inside handlers). One scoped `{Component}.css` /
  `{Component}.ts` sits next to `{Component}.cs` and is auto-included and isolated; orphan or
  ambiguous assets are **RASK015–018**, a `.js` sibling is **RASK055**, and two scoped components sharing a simple type name
  collide at `window.Rask[Name]` (**RASK020**).
- **Put global styles in `wwwroot`, not a scoped CSS file.** Scoped CSS has no opt-out selector, so a
  brand palette, `:root` variables or shell tags belong in a plain stylesheet linked from
  your App's `Head` (use `LiveOptions.PathBase` for the URL). See [JS interop → scoped
  CSS](https://rask.sh/docs/guides/js-interop/#scoped-css).

## Security

- **Order middleware `UseAuthentication()` → `UseAuthorization()` → `MapRask<App>()`.** Rask seeds
  the session from `HttpContext.User` on the initial GET and the WS upgrade; if auth runs *after*
  Rask the principal is empty and every `[Authorize]` page challenges. This is **RASK024**. Behind a
  reverse proxy, wire `UseForwardedHeaders()` *first* so the origin checks see the public host.
- **The session is a cookie, and nothing of it reaches JavaScript.** `Rask.Auth` owns the scheme and
  sets it `HttpOnly`, `Secure`, `SameSite=Lax`, so there is no token for XSS to steal and none to
  store in the browser. Don't reintroduce one by hand.
- **Gate interactive WASM content that renders before its principal lands with the `Authorize`
  component, not route `[Authorize]`.** Route gating runs once, before the page renders; a WASM
  provider is still hydrating then, so `Authorize(Authorized:, NotAuthorized:, Authorizing:)` — whose
  `Authorizing` slot covers exactly that window — is the right tool.
- **Lean on the built-in URL sanitization.** URL-bearing attributes neutralize dangerous schemes
  (`javascript:`, `vbscript:`) to `about:blank` by default; use `RaskUrl.Trusted(...)` only for URLs
  you control. Treat the **session id as a bearer secret** (HTTPS only, never logged), and set a
  strict **Content-Security-Policy** as middleware before `MapRask` — Rask needs no
  `script-src 'unsafe-inline'` (only `style-src 'unsafe-inline'` for `Style:` attributes, plus
  `'wasm-unsafe-eval'` on WASM). Full flows and the [security
  checklist](https://rask.sh/docs/guides/authentication-hardening/#security-checklist) live in [authentication](https://rask.sh/docs/guides/authentication/).

## Accessibility

- **Always give `Img` an `Alt`.** A meaningful string for informative images, `Alt: ""` for
  decorative ones (so screen readers skip them). A missing alt is **RASK023**. See
  [accessibility](https://rask.sh/docs/guides/accessibility/#images-and-alt-text-rask023).
- **Reach the full ARIA vocabulary through the `Aria` dictionary**, with typed `Role` and `TabIndex`
  for the two attributes that aren't `aria-*`. Build higher-level affordances from these primitives
  plus semantic HTML (`Nav`, `Main`, `Label.For(…)`, `Th.Scope(…)`):
  ```csharp
  Div.Role("status").Aria(new() { ["live"] = "polite" })[_statusMessage]
  ```

## Performance & memory

- **Key your lists — it's a performance rule too.** Keyed insert/remove/move ship as small *trusted*
  diff ops that preserve DOM identity. Without keys an append, a truncation, or ONE child (or run of
  children) coming or going among siblings that still match is a diff of exactly those nodes.
- **A child that changes element costs its parent, not the page — and a `Key` there is about identity,
  not bytes.** `cond ? A : B`, words that become an element, a loading line that becomes a table, the
  page in an `Outlet`: the parent's children travel as one subtree morph, and nothing outside that
  parent is touched. So keep what changes shape inside a small element of its own (a status `<span>`'s
  wrapper, the `<main>` around the outlet) rather than directly under `<body>`, where it is still the
  whole page. Inside the morphed parent, children are paired by position: give a sibling a `.Key(…)`
  when it must stay the node it was — a field being typed in, a scrolled list, a playing video — while
  something beside it changes shape. In Development each morphed parent is named on `Rask.Live`. See
  [architecture → the fallback ladder](https://rask.sh/docs/guides/live-rendering-codec/#the-fallback-ladder-fine-diff--one-run--subtree-morph--whole-page).
- **Treat `Key` as identity, not a reactive signal.** Changing a key mounts a fresh instance; it
  doesn't refire `OnUpdated`.
- **Use a `[...]` collection expression to avoid a wrapper node** for sibling lists, and `null` for a
  "render nothing" branch (`show ? Panel() : null`).
- **Benchmark every render-hotpath or live-runtime change.** Diff codec, frame writer, serializer,
  and dispatch are under measurement — run `tests/Rask.Benchmarks` before/after and quote the
  `Allocated` delta. See [development workflow](https://rask.sh/docs/guides/development-workflow/).

## Testing

- **Unit-test first; reach for E2E only when a path is genuinely unreachable** by a unit test (the
  real JS transports, `rask.js` DOM application, real auth handshakes, browser layout). E2E is heavy.
- **Render with the right entry point.** `ToHtml()` for a standalone component; wrap in
  `StubComponent` and call `RenderAsLiveRoot()` for anything needing a live context (handlers, forms,
  DI). Drive handlers via the `data-rask-on-*` id + `TryInvokeHandlerAsync`, and assert exact
  attribute order. See [testing](https://rask.sh/docs/guides/testing/).
- **Every `src/Rask.Site` change gets an E2E test.** Add a Playwright journey to
  `tests/Rask.Site.E2E.Tests`.

## Common pitfalls

| Pitfall | Do instead |
|---|---|
| List items with no `.Key(…)` (**RASK022**) — focus/input lost on reorder | Chain a stable, unique `.Key(…)` (entity id) |
| `new Counter()` outside Core (**RASK014**) | Name it and chain: `Counter`, `Counter.Value(3)` |
| Service as a settable property → required step (**RASK002**) | Inject via the constructor |
| `Head[Title[...]]` (**RASK019**) | Override `protected override Component? HeadAssets` |
| Root renders `Doctype`/`Html`/`Head`/`Body` (**RASK021**) | Return the body's content; `Head`/`HtmlLang`/`BodyClass`/`Shell` |
| User input through `Raw(...)` (XSS) | Use a plain string / `Text` (encodes by default) |
| `StateHasChanged()` inside an awaited handler/hook | Redundant — the `await` re-renders for you |
| `StateHasChanged()` in `OnUnmount` | No-op by design — only tear down subscriptions |
| Subscribing to an event without unsubscribing | Pair `+=` in `OnMount` with `-=` in `OnUnmount` |
| Scoped `DbContext` in a Server app | `IDbContextFactory<T>` + a fresh context per op |
| Async EF/HTTP calls without the token | Thread `Component.CancellationToken` through |
| Any auth token in `localStorage` | The `HttpOnly` session cookie the battery already sets |
| `UseAuthentication()` after `MapRask()` (**RASK024**) | Auth → Authorization → Rask, in that order |
| `Img` without `Alt` (**RASK023**) | Real alt text, or `Alt: ""` for decorative |
| `for`-loop index captured in a binding lambda | Copy to a per-iteration local, or use `foreach` |

---

See also the [diagnostics reference](https://rask.sh/docs/guides/diagnostics/) for every RASK0xx ID and its fix, and
[CLAUDE.md](https://github.com/pal-tamas/rask/blob/main/CLAUDE.md) for the contributor-facing map of the framework internals.
