Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Build LiveView: async data (assign_async), PubSub (check connected?), phx-change events, form components/modals/uploads, streams for lists, live_patch. Use when handling interactions, debugging events, or tracking Presence.
.claude/skills/oliver-kriska-liveview-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 19% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 31% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 8% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 40% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 57% | 0% |
> Ash projects: Use ash-framework skill for AshPhoenix.Form. Lifecycle: AshPhoenix.Form.validate/3 on phx-change, AshPhoenix.Form.submit/2 on submit, to_form/1 for HEEx. Do not use Ecto.Changeset.cast/3.
Reference for building with Phoenix LiveView 1.0/1.1.
assign_async. SEO routes: connected? guard + cache-backed disconnected branch (crawlers read that HTML){:error, changeset} first, not viewport/JShidden_input if not directly editableassign_new FOR LIFECYCLE VALUES — assign_new skips the function if key exists. Use assign/3 for locale, current user, or any value refreshed every mount{:error, %Ecto.Changeset{}} EXPLICITLY — Bare {:error, _} merges changeset and non-changeset errors; the form silently never re-renders validation errors. Handle other errors separately| Pattern | 3K items | 10K users × 10K items | |---------|----------|----------------------| | Regular assigns | ~5.1 MB | ~10+ GB | | Streams | ~1.1 MB | Minimal (O(1)) |
Decision: Lists with >100 items → Use streams, not assigns
elixirdef mount(%{"slug" => slug}, _session, socket) do # Extract needed values BEFORE the closure scope = socket.assigns.current_scope {:ok, socket |> assign_async(:org, fn -> {:ok, %{org: fetch_org(scope, slug)}} end)} end
elixirdef mount(_params, _session, socket) do {:ok, stream(socket, :items, Items.list_items())} end # Insert/update/delete stream_insert(socket, :items, item, at: 0) stream_delete(socket, :items, item)
For public/SEO-visible routes (marketing, articles, product listings) the disconnected render IS the HTML crawlers see. Fetch from a cache there, real data on connect:
elixirdef mount(_params, _session, socket) do products = if connected?(socket), do: Catalog.list_products(), else: Cache.get_products() || [] {:ok, assign(socket, products: products)} end
Empty list → <noscript>-friendly skeleton. Cache → :persistent_term, ETS, or Cachex. This satisfies Iron Law #1 AND keeps Googlebot/GPTBot happy.
elixirdef mount(_params, _session, socket) do if connected?(socket), do: Chat.subscribe(room_id) {:ok, socket} end
Same LiveView, different params? → patch / push_patch
Different LiveView, same live_session? → navigate / push_navigate
Different live_session or non-LiveView? → href / redirectDoes component need BOTH internal state AND event handling?
│
├── YES → Does it encapsulate APPLICATION logic (not just DOM)?
│ ├── YES → Use LiveComponent ✅
│ └── NO → Refactor to function component with parent handling
│
└── NO → Use Function Component ✅Official guidance: "Prefer function components over live components"
| Wrong | Right | |-------|-------| | DB queries without assign_async | Use assign_async for all queries | | assign(socket, items: list) for lists | stream(socket, :items, list) | | PubSub subscribe without connected? | if connected?(socket), do: subscribe() | | Passing socket to context functions | Extract socket.assigns first | | Business logic in handle_event | Delegate to context | | assign_new for locale/user in hooks | assign/3 (must run every mount) |
For detailed patterns, see:
references/async-streams.md - assign_async, stream_async, streamsreferences/forms-uploads.md - Forms, validation, file uploadsreferences/components.md - Function components, LiveComponentsreferences/pubsub-navigation.md - PubSub, navigation, JS commandsreferences/js-interop.md - Third-party JS libraries, phx-update="ignore", hooksreferences/channels-presence.md - Phoenix Channels, Presence, token auth| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 18,031 | 14,876 | -17% | 1 | 1 | 0% | 3,421 | 4,063 | +19% | 0 | 0 | — |
case-02 | pass→pass | 18,328 | 14,948 | -18% | 1 | 1 | 0% | 3,673 | 4,259 | +16% | 0 | 0 | — |
case-03 | fail→pass | 19,954 | 18,987 | -5% | 1 | 1 | 0% | 4,078 | 5,330 | +31% | 0 | 0 | — |
case-04 | pass→pass | 12,348 | 4,494 | -64% | 1 | 1 | 0% | 2,267 | 2,128 | -6% | 0 | 0 | — |
case-05 | pass→pass | 20,112 | 13,742 | -32% | 1 | 1 | 0% | 4,020 | 4,020 | 0% | 0 | 0 | — |
case-06 | pass→pass | 10,078 | 5,247 | -48% | 1 | 1 | 0% | 1,849 | 2,290 | +24% | 0 | 0 | — |
case-07 | fail→pass | 15,707 | 9,849 | -37% | 1 | 1 | 0% | 2,775 | 2,999 | +8% | 0 | 0 | — |
case-08 | pass→fail | 13,922 | 10,297 | -26% | 1 | 1 | 0% | 2,800 | 3,418 | +22% | 0 | 0 | — |
case-09 | fail→pass | 15,748 | 13,517 | -14% | 1 | 1 | 0% | 2,696 | 3,769 | +40% | 0 | 0 | — |
case-10 | pass→pass | 12,338 | 7,352 | -40% | 1 | 1 | 0% | 2,288 | 2,695 | +18% | 0 | 0 | — |
case-21 | pass→pass | 16,954 | 11,277 | -33% | 1 | 1 | 0% | 3,260 | 3,549 | +9% | 0 | 0 | — |
case-11 | pass→pass | 12,073 | 7,855 | -35% | 1 | 1 | 0% | 2,070 | 2,742 | +32% | 0 | 0 | — |
case-12 | pass→pass | 16,444 | 10,526 | -36% | 1 | 1 | 0% | 2,891 | 3,329 | +15% | 0 | 0 | — |
case-13 | pass→pass | 10,857 | 5,300 | -51% | 1 | 1 | 0% | 1,855 | 2,345 | +26% | 0 | 0 | — |
case-14 | pass→pass | 8,671 | 4,669 | -46% | 1 | 1 | 0% | 1,463 | 2,138 | +46% | 0 | 0 | — |
case-20 | pass→pass | 7,631 | 4,433 | -42% | 1 | 1 | 0% | 1,407 | 2,178 | +55% | 0 | 0 | — |
case-15 | pass→pass | 6,391 | 3,495 | -45% | 1 | 1 | 0% | 1,101 | 1,948 | +77% | 0 | 0 | — |
case-16 | pass→pass | 12,642 | 8,398 | -34% | 1 | 1 | 0% | 2,115 | 2,893 | +37% | 0 | 0 | — |
case-17 | fail→pass | 10,215 | 7,261 | -29% | 1 | 1 | 0% | 1,752 | 2,757 | +57% | 0 | 0 | — |
case-18 | pass→pass | 5,529 | 5,040 | -9% | 1 | 1 | 0% | 1,032 | 2,255 | +119% | 0 | 0 | — |
case-19 | pass→pass | 15,812 | 11,022 | -30% | 1 | 1 | 0% | 2,874 | 3,451 | +20% | 0 | 0 | — |
case-22 | pass→pass | 7,093 | 4,407 | -38% | 1 | 1 | 0% | 1,238 | 2,143 | +73% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +18 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.