Back to catalog

liveview-guidelines

Provides Phoenix LiveView best practices including navigation, streams, JavaScript hooks, and testing. Use when building LiveViews, working with real-time collections, managing streams, or writing LiveView tests.

  • Category: discovered
  • Tags: github, community
  • Version: 1.0.0
  • Author: walid
  • Downloads: 0
  • GitHub stars: 1
  • Updated: 2025-11-21

Download the latest version (v1.0.0) from this page.

SKILL.md

---
name: liveview-guidelines
description: Provides Phoenix LiveView best practices including navigation, streams, JavaScript hooks, and testing. Use when building LiveViews, working with real-time collections, managing streams, or writing LiveView tests.
---

# Phoenix LiveView Guidelines

This skill provides best practices for building Phoenix LiveView applications.

## Navigation

### Modern Navigation Functions

- **Never** use deprecated `live_redirect` and `live_patch` functions
- **Always** use these alternatives:
  - In templates: `<.link navigate={href}>` and `<.link patch={href}>`
  - In LiveViews: `push_navigate/2` and `push_patch/2`

```heex
<%!-- Template navigation --%>
<.link navigate={~p"/users"}>View Users</.link>
<.link patch={~p"/users/new"}>New User</.link>
```

```elixir
# LiveView navigation
{:noreply, push_navigate(socket, to: ~p"/dashboard")}
{:noreply, push_patch(socket, to: ~p"/users/#{user.id}")}
```

## LiveComponents

- **Avoid LiveComponents** unless you have a strong, specific need for them
- Function components are usually sufficient and simpler
- LiveComponents add complexity with lifecycle callbacks and state management

## Naming and Routes

### LiveView Naming

LiveViews should be named with a `Live` suffix: `AppWeb.WeatherLive`

### Route Definition

The default `:browser` scope is **already aliased** with `AppWeb`, so use:

```elixir
live "/weather", WeatherLive
```

Not:

```elixir
live "/weather", AppWeb.WeatherLive  # Redundant!
```

## JavaScript Hooks

When using `phx-hook="MyHook"` and the hook manages its own DOM, you **must** also set `phx-update="ignore"`:

```heex
<div id="chart" phx-hook="ChartHook" phx-update="ignore">
  <%!-- Hook manages this DOM --%>
</div>
```

### Script Organization

- **Never** write embedded `<script>` tags in HEEx templates
- **Always** write scripts and hooks in `assets/js` directory
- Integrate hooks with `assets/js/app.js`

## LiveView Streams

### When to Use Streams

**Always** use LiveView streams for collections instead of regular lists to avoid memory ballooning and runtime termination.

### Stream Operations

```elixir
# Basic append
stream(socket, :messages, [new_msg])

# Reset stream (for filtering/refreshing)
stream(socket, :messages, filtered_messages, reset: true)

# Prepend
stream(socket, :messages, [new_msg], at: -1)

# Delete item
stream_delete(socket, :messages, msg)
```

### Stream Template Syntax

Template **must** set `phx-update="stream"` on parent element with a DOM id. Consume `@streams.stream_name` and use id as DOM id for each child:

```heex
<div id="messages" phx-update="stream">
  <div :for={{id, msg} <- @streams.messages} id={id}>
    {msg.text}
  </div>
</div>
```

### Stream Limitations

**Not Enumerable**: LiveView streams are **not enumerable**—cannot use `Enum.filter/2`, `Enum.reject/2`, or similar functions.

To filter/prune/refresh items, **must refetch data and re-stream with `reset: true`**:

```elixir
def handle_event("filter", %{"filter" => filter}, socket) do
  messages = list_messages(filter)

  {:noreply,
   socket
   |> assign(:messages_empty?, messages == [])
   |> stream(:messages, messages, reset: true)}
end
```

### Empty States

Streams **do not support counting or empty states** directly.

- Track counts using separate assigns
- For empty states, use Tailwind classes:

```heex
<div id="tasks" phx-update="stream">
  <div class="hidden only:block">No tasks yet</div>
  <div :for={{id, task} <- @streams.tasks} id={id}>
    {task.name}
  </div>
</div>
```

This only works if the empty state is the only HTML block alongside the stream for-comprehension.

### Deprecated

- **Never** use deprecated `phx-update="append"` or `phx-update="prepend"` for collections

## LiveView Tests

### Testing Tools

- Use `Phoenix.LiveViewTest` module for LiveView testing
- Use `LazyHTML` (included) for making assertions
- Form tests use `render_submit/2` and `render_change/2`

### Test Organization

- Create step-by-step test plan splitting major cases into small, isolated files
- Start with simpler tests verifying content exists
- Gradually add interaction tests

### Best Practices

**Reference Element IDs**:

```elixir
assert has_element?(view, "#product-form")
assert has_element?(view, "#submit-button")
```

**Never test against raw HTML**:

```elixir
# BAD
assert html =~ "Product Form"

# GOOD
assert has_element?(view, "#product-form")
```

**Test element presence over text content**:
- Text content can change
- Element IDs are more stable

**Focus on outcomes, not implementation details**

**Be aware of component output**:
- `Phoenix.Component` functions might produce different HTML than expected
- Test against the actual output HTML structure

### Debugging Test Failures

When facing test failures with element selectors, use `LazyHTML` to debug:

```elixir
html = render(view)
document = LazyHTML.from_fragment(html)
matches = LazyHTML.filter(document, "your-complex-selector")
IO.inspect(matches, label: "Matches")
```

## Summary

- Use modern navigation: `<.link navigate>`, `push_navigate`
- Avoid LiveComponents unless necessary
- Name LiveViews with `Live` suffix
- Use `phx-update="ignore"` with hooks that manage DOM
- Always use streams for collections
- Reset streams to filter: `stream(socket, :items, items, reset: true)`
- Test with element IDs, not raw HTML
- Use `LazyHTML` for debugging test failures