Here's a collection of useful Phoenix LiveView tidbits. I thought that the format and easy grok-ability of asking questions would be valuable. LiveView
's documentation is fantastic and thorough, but you got to read all of it to get all of it. And we all know we don't read every line of published documentation :)
The first draft was aggregated from ~10 days of Elixir Slack's #liveview
channel. Thanks to everyone who asks questions and helps out!
- Why is
mount/3
being called twice? - I'm fetching the
current_user
inmount
and it's being fetched... twice ๐ค๐ค - I want my
LiveView
to be rendered once and only once - I want to upload a file!
- My form isn't tracking changes! ๐
- Nothing is tracking changes!!! ๐ก๐ก๐ก
- Is pagination a thing?
- I'm trying to redirect in a callback but it's being ignored ๐ค
- No, really, my redirect isn't working in a callback ๐ค๐ค
- Can I style live flash messages?
- But what does my
LiveView
process state really look like? - I'm using
live_action
s and my modules are getting large and unwieldly ๐ - Why can't I send a message to my
LiveComponent
process? - Why does Phoenix 1.5 generate a
root.html.leex
if it doesn't track changes? - Is the
user_id
in mysocket.assigns
secure? Can it be tampered with? ๐ฎ - Where are my
LiveView
routes? - My stateless
LiveComponent
is sending all myassigns
over the wire!! - My entire user is in
Plug.Session
so I don't have to make database calls inLiveView
. This is a good thing, right? - Got anything else? ๐ฅบ
Straight from the docs themselves:
For each LiveView in the root of a template, mount/3 is invoked twice: once to do the initial page load and again to establish the live socket.
The important phrase here being "the root of the template". In this case:
scope "/", AppWeb do
live "/", PageLive
end
PageLive
's mount/3
is called twice on the first navigation to "/"
. Once you navigate to another LiveView
with live_redirect
, however, mount/3
is called once.
Note that if you're performing some expensive operation in mount/3
, take a look at connected?
. A check to see if the socket is connected to defer the work is fine when it's needed.
Which is expected, see above :D If you are fetching it in a plug you're actually fetching it three times... the horror! To solve this, reach for assign_new/3
. This allows you a few niceties such as sharing connection assigns on the initial HTTP request and only setting an assign if it's not available to children components.
This, admittedly, took me a while to wrap my head around. Nothing helps like a (contrived) example!
# in a plug or controller
conn
|> assign(:foo, "BAR")
defmodule AppWeb.PageLive do
use AppWeb, :live_view
def render(assigns) do
~L"""
<div>assign foo is <%= @foo %></div>
"""
end
def mount(_params, _session, socket) do
{:ok, assign_new(socket, :foo, fn -> "bar" end)}
end
end
Here's the logic of the :foo
assign:
- On the first disconnected mount,
PageLive
will render"BAR"
in place of@foo
because the key is already available in theconn
- On the second connected mount, it will render
"bar"
because there are no parent assigns to pull from
Now, let's add and render a stateful LiveComponent
and see how assign_new
behaves:
# in a plug or controller
conn
|> assign(:foo, "BAR")
defmodule AppWeb.PageLive do
use AppWeb, :live_view
def render(assigns) do
~L"""
<div>assign foo is <%= @foo %></div>
<%= live_component(@socket, AppWeb.PageLiveChild, id: "page-live-child", bar: @bar) %>
"""
end
def mount(_params, _session, socket) do
socket =
|> assign_new(:foo, fn -> "bar" end)
|> assign(:bar, "FOO")
{:ok, socket}
end
end
defmodule AppWeb.PageLiveChild do
use Phoenix.LiveComponent
def render(assigns) do
~L"""
<div>
<div>assign bar is <%= @bar %></div>
</div>
"""
end
def mount(socket) do
socket =
socket
|> assign_new(:bar, fn -> "wee" end)
{:ok, socket}
end
end
Let's go through the rendering logic for PageLiveChild
:
- On the first disconnected mount,
PageLiveChild
will render"FOO"
in place of@bar
because the key is already available in socket coming in from the parent,PageLive
- On the second connected mount, it will render exactly same thing! That's because
:bar
is still set from the socket assign inPageLive
Seeing mount/3
being called on every live_redirect
but want it to just be called once? Render your component in root.html.leex
like this:
<%= live_render(@conn, AppWeb.PageLive) %>
Now PageLive
will survive redirects.
File uploads aren't supported by LiveView
just yet. Check out Jon Rowe's library Phoenix Live View Dropzone.
LiveView
can't compute diffs instead of anonymous functions, so form_for/4
doesn't work. Make sure you are using form_for/3
.
Make sure you're writing code in a html.leex
file, not html.eex
๐
Not out of the box, but check out joshchernoff's very helpful gist.
Just like assign/3
, redirect/2
annotates and returns the updated socket. Meaning, this won't work:
def handle_info("annotate_redirect", _, socket) do
redirect(socket, to: Routes.some_path())
{:noreply, socket}
end
But this will:
def handle_info("annotate_redirect", _, socket) do
socket = redirect(socket, to: Routes.some_path())
{:noreply, socket}
end
Extra credit if you noticed this is also an option:
def handle_info("annotate_redirect", _, socket) do
{:noreply, redirect(socket, to: Routes.some_path())}
end
Remember: data is immutable in Elixir!
Note that redirect/2
requires you take action on the provided redirect location. If you want to redirect right from the server, use push_patch/2
or push_redirect/2
.
Absolutely! In a fresh Phoenix app generated with the --live
flag, you can see this in templates/layout/live.html.leex
:
<p class="alert alert-info" role="alert"
phx-click="lv:clear-flash"
phx-value-key="info"><%= live_flash(@flash, :info) %></p>
If you'd like to conditionally display it and wrap it in more complicated markup, try:
<%= if message = live_flash(@flash, :info) do %>
<div class="alert alert-info" role="alert" phx-click="lv:clear-flash" phx-value-key="info">
<div class="some-class">
<div class="another-class">
<%= message %>
</div>
</div>
</div>
<% end %>
Check out toranb's quick blog post on inspecting LiveView
process state.
Instead of separating logic via live_action
s in the router like this:
scope "/", AppWeb do
live "/", PageLive, :index
live "/edit", PageLive, :edit
live "/foo", PageLive, :foo
end
Pull them out into separate, name-spaced modules:
scope "/", AppWeb do
live "/", PageLive.Index
live "/edit", PageLive.Edit
live "/foo", PageLive.Foo
end
LiveComponent
s aren't in their own process, only LiveView
s are. If you call self()
in a LiveComponent
you'll get back that PID
of the LiveView
parent.
It can be used to track changes later if you want to render a LiveView
inside of it. Mainly, it's to reduce confusion ๐
Is is secure. External clients have no access to it as long as your signing secrets are safe.
Given:
live "/foo/new", FooLive.New, :new
resources "/foo", FooController, only: [:index, :create, :show]
It would be easy to assume that Routes.foo_path(@conn, :new)
would generate a link that would bring us to our LiveView
. However, that's not the case. The full module name, namespace and all, will be taken to account so the path you'd want to use is actually Routes.foo_new_path(@conn, :new)
. Remember to check mix phx.routes
if you're having issues finding paths! All live and dead routes will be listed.
update/2
merges assigns into the socket, then render/1
is called with all assigns. Thus, no change tracking occurs. If you've got a stateless LiveComponent
with a lot of assigns, consider:
- Making it stateful by passing an
:id
to the component - Abstract the most updated assign into a stateful component and past the rest of the assigns to a stateless component
My entire user is in Plug.Session
so I don't have to make database calls in LiveView
. This is a good thing, right?
No :) It's unlikely you want the whole %User{}
struct. There could be other metadata added to it (see pow
), cache busting gets thrown out the door, and you're subject to cookie overflow.
Check out Awesome Phoenix Liveview.