https://fly.io/phoenix-files/phoenix-dev-blog-streams/
App performance optimization Open main menu
Articles
Blog Phoenix Files Laravel Bytes Ruby Dispatch
Content
Blog Phoenix Files Laravel Bytes Ruby Dispatch
Docs Community Status Pricing Sign In Get Started RSS Feed
Reading time * 5 min Share this post on Twitter Share this post on
Hacker News Share this post on Reddit
Phoenix Dev Blog - Streams
Author
Chris McCord
Name
Chris McCord
Social Media
@chris_mccord View Twitter Profile
Phoenix dev blog cover illustration Image by Annie Ruygt
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. Fly.io is a great place to run a Phoenix
application! Check out how to get started!
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 it sucked
even when it worked. Let's see why.
Today it works by marking an assign as "temporary", which means the
server throws it away after rendering it. Then to append a new item,
we allow the developer to render only the new items, and the client
would automagically leave the old ones in place instead of removing
them. In practice it looked like this:
def render(assigns) do
~H"""
-
"""
end
def mount(_, _, socket) do
users = Accounts.list_users()
- {:ok, assign(socket, users: users), temporary_assigns: [users: []]
+ {:ok, stream(socket, :users, users)}
end
def handle_info({:user_added, new_user}) do
- {:noreply, assign(socket, users: [new_user])}
+ {:noreply, stream_insert(socket, :users, new_user)}
end
In mount/3, we define a stream with stream/3. Streams clean up after
themselves, so there is no need to mess with temporary assigns
yourself. Like before, streams identify their items by DOM id. By
default, it will use the item :id field if the item is a map or
struct with such a field. The following two lines are equivalent:
stream(socket, :users, users)
stream(socket, :users, users, dom_id: &"users-#{&1.id}")
Next, in the template in render/1, we mark the container as
phx-update="stream", then we use a regular for comprehension, but
with two changes. Streams are placed under a @streams assign, and
when you enumerate a stream you get the computed DOM id along with
each item. We then render the DOM id and content as before.
Finally, in handle_info/2 we see the stream interface in action.
stream_insert allows inserting or updating items in the stream. By
default, items will be appended on the client, but you can
programmatically place them with the :at option, which mimics the
behavior of Elixir's List.insert_at. The following two lines are
equivalent:
stream_insert(socket, :users, new_user)
stream_insert(socket, :users, new_user, at: -1)
To prepend the new user in the UI instead:
stream_insert(socket, :users, new_user, at: 0)
You can also place the user at an arbitrary index, which makes
reordering items in the UI a breeze.
For deletes, stream_delete works as you'd expect:
stream_delete(socket, :users, user)
Here's a fully realized example of what streams unlock for LiveView
developers. We updated our flagship LiveBeats example app to use
streams for its playlist, with drag and drop re-ordering, deletion,
and more:
Should streams Be Used by Default Now for Lists of Items?
Streams by default for any kind of collection is a good intuition to
have. You should use streams any time you don't want to hold the list
of items in memory - which is most times. Streams are also a goto
when you want to efficiently update a single list item without
refactoring to a layer of LiveComponents for the items.
Streams Retrospective
There's something really satisfying about implementing a long-term
feature, then shedding all that knowledge by being a user of the
feature. LiveView features continue to do this kind of thing to me. I
wrote it all - and it still feels like magic when using it!
After playing with the top-level stream API as a user, I am also
struck by how simple it is. I constantly wonder "how did it take this
long to do this?", but then you look at the PR. It touched every
layer of the LiveView stack - it required features/additions to the
HTML engine at the parser level, the diffing engine, the client diff
merging, and patches to morphdom.
The best thing about streams is the internal implementation is
optimized for both the server and the client. We introduced new
features in morphdom to drop all the fake DOM tree hacks from the
previous approach.
I'm excited to finally offer a comprehensive solution to an area I
was never really satisfied with before. I can't wait to see what
folks ship with this!
Happy hacking!
-Chris
Fly.io [?] Elixir
Fly.io is a great way to run your Phoenix LiveView app close to your
users. It's really easy to get started. You can be running in
minutes.
Deploy a Phoenix app today! -
[cta-kitty]
Last updated *
Feb 27, 2023
Share this post on Twitter Share this post on Hacker News Share this
post on Reddit
Author
Chris McCord
Name
Chris McCord
Social Media
@chris_mccord View Twitter Profile
Previous post |
Phoenix LiveView and SQLite Autocomplete
Previous post |
Phoenix LiveView and SQLite Autocomplete
App performance optimization
Company
About Pricing Jobs
Articles
Blog Phoenix Files Laravel Bytes Ruby Dispatch
Resources
Docs Support Status
Contact
GitHub Twitter Community
Legal
Security Privacy policy Terms of service
Copyright (c) 2023 Fly.io