Phoenix Dev Blog - Streams · The Phoenix Files

LiveView’s New Streams Feature

This dev blog introduces LiveView’s new Streams feature. It lets us elegantly work with large collections of items without keeping them all in memory on the server. This is the first installment of the Phoenix development blog where we’ll talk about in-progress features or day-to-day development updates in between major releases and milestones.

What’s the Problem?

For at least a few years, the Phoenix team has wanted a solution that elegantly addresses large collections of items without requiring the collection to live in memory on the server. We’ve had a hack in place by allowing a container to be marked with phx-update="append" or phx-update="prepend". It worked for some use cases but was limited.

The append/prepend trick allowed developers to start with a “naive” in-memory store of the collection, then optimize it without changing much of their code.


def render(assigns) do
  ~H"""
  <div id="users" phx-update="append">
    <div :for={user <- @users} id={"user-#{user.id}"}>  
      <%= user.name %>
    </div>
  </div>
  """
end

def mount(_, _, socket) do
  users = Accounts.list_users()
  {:ok, assign(socket, users: users), temporary_assigns: [users: []]}  
end

def handle_info({:user_added, new_user}) do
  {:noreply, assign(socket, users: [new_user])}
end

Great! Everyone is happy, except this approach had several drawbacks:

  1. Deletions were not supported without extra JavaScript.
  2. Limited modes of operation: only append or prepend.
  3. No easy way to sort items or manage updates.
  4. The internal implementation was expensive and brittle.

Enter Streams

We are introducing a new “streams” feature to solve the issues above. New Phoenix 1.7 applications will use streams out of the box for the phx.gen.live LiveView generators.

Streams support dynamic ordering, which makes appending, prepending, or reordering trivial for the developer. Deletes are also just as simple. Let’s refactor our original example to utilize streams:


def render(assigns) do
  ~H"""
  <div id="users" phx-update="stream">
    <div :for={{id, user} <- @streams.users} id={id}>
      <%= user.name %>
    </div>
  </div>
  """
end

def mount(_, _, socket) do
  users = Accounts.list_users()
  {:ok, stream(socket, :users, users)}
end

def handle_info({:user_added, new_user}) do
  {:noreply, stream_insert(socket, :users, new_user)}
end

In mount/3, we define a stream with stream/3. Streams clean up after themselves, which means no more manual management of temporary assigns.

Here’s how to insert or update items:

stream_insert(socket, :users, new_user)

To prepend the new user in the UI:

stream_insert(socket, :users, new_user, at: 0)

For deletes, stream_delete works as you’d expect:

stream_delete(socket, :users, user)

Should streams be used by default now for lists of items?

Using streams for any kind of collection is a recommended practice, particularly when you want to avoid holding the list of items in memory. Streams are efficient for updating single list items without the need for deeper refactoring.

Streams Retrospective

There’s a satisfaction in implementing a long-term feature and experiencing its capabilities as a user. The internal implementation for streams has seen optimizations for both the server and the client.

I’m excited to offer this comprehensive solution to a problem area I was never satisfied with before.

Happy hacking!

–Chris