igmarin/elixir-phoenix-skills

phoenix-liveview-essentials

MANDATORY for ALL LiveView work. Invoke before writing LiveView modules or .heex templates. Covers the two-phase rendering lifecycle, mount/handle_event/handle_info/handle_params callbacks, socket assigns, streams, components, form binding, error handling, and PubSub integration. Trigger words: LiveView, live_view, mount, handle_event, handle_info, render, HEEx, socket, assign.

First seen Jun 20, 2026

Installation

$ npx skills add igmarin/elixir-phoenix-skills --skill phoenix-liveview-essentials

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from igmarin/elixir-phoenix-skills · top by installs.

npx skills add igmarin/elixir-phoenix-skills

Browse all from igmarin/elixir-phoenix-skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 2
License LICENSE
Default branch main
Open issues 0
Status Active

Skill metadata

Parsed from SKILL.md frontmatter.

Version1.0.0
LicenseMIT
More metadata
version
1.0.0
user-invocable
true

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 11,085 B
  • docs SUMMARY.md 415 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 22 installs

SKILL.md

Phoenix LiveView Essentials

Use this skill before writing ANY LiveView module or .heex template.

Canonical FP bar: [docs/fcis-engineering-rules.md](../../docs/fcis-engineering-rules.md) — Functional Core, Imperative Shell: pure domain modules; side effects at edges. Keep LiveView/controller callbacks thin; delegate business rules to contexts/pure modules.

RULES — Follow these with no exceptions

1. Always add @impl true before every callback (mount, handleevent, handleinfo, render) 2. Initialize assigns before they're accessed in render/1 — use mount/3 for static defaults, handleparams/3 for URL-dependent assigns 3. Check connected?(socket) before PubSub subscriptions, timers, or side effects 4. Use Map.get(assigns, :key, default) for optional assigns in helper functions 5. Return proper tuples — {:ok, socket} from mount, {:noreply, socket} from handleevent 6. Use with for error handling in event handlers — assign errors to socket, don't crash 7. Never use autoupload: true with form submission — manual uploads are required to control when files are consumed relative to form data 8. Check corecomponents.ex for existing components before creating custom ones 9. Never query the database directly from LiveViews — call context functions instead 10. Use streams for large collections — see liveview-streams skill for details

FCIS at this boundary

LiveView callbacks are the imperative shell. Delegate business rules to contexts/pure modules; only assign results and handle UI errors here.

❌ Bad: heavy logic in handleevent/3 (also uses legacy currentuser — prefer current_scope)

@impl true
def handle_event("save", %{"post" => params}, socket) do
  title = String.trim(params["title"] || "")
  status = if params["publish"] == "true", do: "published", else: "draft"
  # legacy assign shape — do not copy; use current_scope in real apps
  {:ok, post} = Repo.insert(%Post{title: title, status: status, user_id: socket.assigns.current_user.id})
  {:noreply, assign(socket, :post, post)}
end

✅ Good: thin event → context → assign

@impl true
def handle_event("save", %{"post" => params}, socket) do
  case Blog.create_post(socket.assigns.current_scope, params) do
    {:ok, post} ->
      {:noreply, socket |> assign(:post, post) |> put_flash(:info, "Saved")}

    {:error, %Ecto.Changeset{} = changeset} ->
      {:noreply, assign(socket, :form, to_form(changeset))}
  end
end

Recommended Build Order

  1. Define mount/3 — initialize all assigns with static defaults
  2. Add handle_params/3 — set URL-dependent assigns, subscribe to PubSub
  3. Write render/1 — reference only assigns initialized in steps 1–2
  4. Add handle_event/3 — implement user interactions with proper error handling
  5. Verify static render — confirm no KeyError before WebSocket connects

Critical Concept: Two-Phase Rendering

Both phases run mount → handle_params → render. Initialize all assigns to safe defaults in Phase 1 (connected?(socket) is false) so the static HTML never raises a KeyError. Side effects, PubSub, and timers only work in Phase 2 (connected?(socket) is true).

Mount Callback

@impl true
def mount(_params, _session, socket) do
  socket =
    socket
    |> assign(:user, nil)
    |> assign(:loading, false)
    |> assign(:post_id, nil)
    |> assign(:data, [])

  if connected?(socket) do
    Phoenix.PubSub.subscribe(MyApp.PubSub, "topic")
  end

  {:ok, socket}
end

Defer expensive operations to the connected phase:

@impl true
def mount(_params, _session, socket) do
  socket =
    if connected?(socket) do
      assign(socket, :data, run_expensive_query())
    else
      assign(socket, :data, [])
    end

  {:ok, socket}
end

✅ Validation checkpoint: Verify all assigns used in render/1 are initialized — the static render must display without a KeyError.

Handle Event

@impl true
def handle_event("delete", %{"id" => id}, socket) do
  case Blog.delete_post(socket.assigns.current_scope, id) do
    {:ok, _post} ->
      {:noreply, assign(socket, :posts, Blog.list_posts(socket.assigns.current_scope))}

    {:error, _reason} ->
      {:noreply, put_flash(socket, :error, "Could not delete post")}
  end
end

For create/update events, use the Error Handling pattern below — assign changeset errors to the socket rather than raising.

✅ Validation checkpoint: Each handler must return {:noreply, socket}; error paths assign errors to the socket rather than raising.

Handle Info

Match on the message shape and update assigns accordingly:

@impl true
def handle_info({:post_updated, post}, socket) do
  {:noreply, assign(socket, :post, post)}
end

Handle Params

Called in BOTH render phases on URL changes. Place URL-dependent assigns here so they are available in both static and connected renders.

@impl true
def handle_params(%{"id" => id}, _uri, socket) do
  case Blog.fetch_post(socket.assigns.current_scope, id) do
    {:error, :not_found} ->
      {:noreply, push_navigate(socket, to: ~p"/posts")}

    {:ok, post} ->
      old_id = socket.assigns[:post_id]

      socket =
        if connected?(socket) and not is_nil(old_id) and old_id != post.id do
          Phoenix.PubSub.unsubscribe(MyApp.PubSub, "post:#{old_id}")
          socket
        else
          socket
        end

      if connected?(socket) do
        Phoenix.PubSub.subscribe(MyApp.PubSub, "post:#{post.id}")
      end

      {:noreply,
       socket
       |> assign(:post, post)
       |> assign(:post_id, post.id)}
  end
end

@impl true
def handle_params(_params, _uri, socket) do
  {:noreply, socket}
end

Socket Assigns

Assigns drive rendering. Use assign/3 for direct values and update/3 when the new value depends on the current one. In render/1, direct @key access is safe when the assign is initialized in mount; in helper functions, guard optional assigns with Map.get/3 to avoid KeyError.

Single assign:

socket = assign(socket, :count, 0)

Multiple assigns at once:

socket =
  socket
  |> assign(:user, user)
  |> assign(:loading, false)
  |> assign(:posts, [])

Incremental change — depend on the current value:

❌ Bad — reads the assign manually, then re-assigns:

count = socket.assigns.count
assign(socket, :count, count + 1)

✅ Good — update/3 transforms the current value:

update(socket, :count, fn count -> count + 1 end)

Safe helper access:

❌ Bad — a maybe-missing @current_user raises KeyError:

defp format_user(socket) do
  socket.assigns.current_user.name
end

✅ Good — guard optional assigns with Map.get/3:

defp format_user(socket) do
  case Map.get(socket.assigns, :current_user, nil) do
    nil -> "Guest"
    user -> user.name
  end
end

Live Navigation

# Full page reload (new LiveView)
{:noreply, push_navigate(socket, to: ~p"/users")}

# Patch (same LiveView, different params)
{:noreply, push_patch(socket, to: ~p"/posts/#{post}")}

Form Binding

<.simple_form for={@form} phx-change="validate" phx-submit="save">
  <.input field={@form[:title]} label="Title" />
  <.input field={@form[:body]} type="textarea" label="Body" />
  <:actions>
    <.button>Save</.button>
  </:actions>
</.simple_form>
@impl true
def mount(_params, _session, socket) do
  {:ok, assign(socket, form: to_form(Blog.change_post(%Post{})))}
end

@impl true
def handle_event("validate", %{"post" => params}, socket) do
  changeset =
    %Post{}
    |> Blog.change_post(params)
    |> Map.put(:action, :validate)

  {:noreply, assign(socket, form: to_form(changeset))}
end

Error Handling

@impl true
def handle_event("save", %{"post" => post_params}, socket) do
  case Blog.create_post(socket.assigns.current_scope, post_params) do
    {:ok, post} ->
      socket =
        socket
        |> put_flash(:info, "Created!")
        |> assign(:post, post)

      {:noreply, socket}

    {:error, %Ecto.Changeset{} = changeset} ->
      {:noreply, assign(socket, form: to_form(changeset))}

    {:error, reason} ->
      {:noreply, put_flash(socket, :error, "An error occurred: #{reason}")}
  end
end

Related skills: liveview-streams, phoenix-pubsub-patterns, phoenix-liveview-auth, phoenix-scopes, testing-essentials.

When Not to Use

  • Static pages or controller/view patterns — no LiveView module or .heex template involved
  • Large collection rendering (100+ items) — use liveview-streams instead for DOM efficiency
  • Authentication flows — use phoenix-liveview-auth and phoenix-scopes instead

Testing LiveViews

Drive the full lifecycle with Phoenix.LiveViewTest — mount, render, events, and navigation. See [assets/liveviewtesttemplate.md](assets/liveviewtesttemplate.md) for a LiveView test template and [assets/componenttesttemplate.md](assets/componenttesttemplate.md) for a function-component test template.

Common Pitfalls

❌ Don't ✅ Do
Access an assign in render/1 that was never initialized Initialize every assign in mount/3 or handle_params/3 before it is rendered
Subscribe to PubSub or start timers in the static mount Guard side effects with if connected?(socket)
Query the database directly from the LiveView Call context functions (e.g. Posts.list_posts/0) instead
Read an assign then re-assign it to increment Use update/3 to transform the current value
Use @key for an optional assign in a helper Use Map.get(assigns, :key, default) to avoid KeyError
Return a bare socket or the wrong tuple from a callback Return {:ok, socket} from mount, {:noreply, socket} from handlers
Omit @impl true on a callback Add @impl true before mount, handleevent, handleinfo, handle_params

Integration

Predecessor This Skill Successor
elixir-essentials phoenix-liveview-essentials liveview-streams
ecto-changeset-patterns phoenix-liveview-essentials phoenix-liveview-auth

Companion skills: liveview-streams, phoenix-scopes, phoenix-pubsub-patterns, testing-essentials.