Currently, every dynamic data that you want your LV to render needs to be stored in LV’s state as an assign.
This creates some specific challenges when you want your LV’s to consume as little memory as possible so it scales better.
For example, let’s say I have a Property schema and a LV that will load a property and render it.
That Property schema has a bunch of fields (address, baths, beds, square feet, images, etc), but all of these fields are expected to be “static”, meaning that I will render a page showing the property address, but I don’t have any other code that will use it. In other words, I loaded the schema struct just to render it to the user, but I don’t use it to do anything else inside my LV (except, perhaps, the property id field).
This means that I want to render all information about the property in my LV, but there is no real reason to store all that information in the LV’s socket assigns.
This proposal tries to tackle this scenario by allowing the user to add a special assign that is “ephemeral”. It would work like this:
The user has some state that they need to render but don’t need to store in the LV’s state:
def ...(..., socket) do
property = load_property()
socket = socket |> assign(property_id: property.id) |> temp_assign(property: property)
end
def render(assigns) do
~H"""
<div>Address: <%= @temps.property.address %></div>
<div>Square Feet: <%= @temps.property.square_feet %></div>
<div :for={image <- @temps.property.images}>Image: <img src={image} /></div>
...
"""
end
The above code will store property_id inside the socket assigns map as expected, and property into a temps assigns map that will be reset to %{} after the subsequent render.
After that render, unless the temps assigns map has a key property again, it will just ignore all calls to @temps.property in the ~H code.
If, later, the user calls temp_assign(socket, property: property) again with a new property, then the LV’s engine will see that the property key is available again in the temps assigns map again and re-render it.
There are temporary_assigns which was used before streams. Not sure if it will be deprecated or not. I would think not as you can use it for this purpose.
It is not the same thing, temporary_assigns can only be set in a LV’s mount/3 call, you can’t use it inside a live_component or with more modern features like handle_async, etc.
This is not correct. In fact temporary_assigns is exactly what you are proposing here
I’m attaching a single file example that demonstrates temporary assigns both inside a LiveComponent and with handle_async:
Application.put_env(:sample, Example.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 5001],
server: true,
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64)
)
Mix.install([
{:plug_cowboy, "~> 2.5"},
{:jason, "~> 1.0"},
{:phoenix, "~> 1.7"},
# please test your issue using the latest version of LV from GitHub!
{:phoenix_live_view,
github: "phoenixframework/phoenix_live_view", branch: "main", override: true}
])
# build the LiveView JavaScript assets (this needs mix and npm available in your path!)
path = Phoenix.LiveView.__info__(:compile)[:source] |> Path.dirname() |> Path.join("../")
System.cmd("mix", ["deps.get"], cd: path, into: IO.binstream())
System.cmd("npm", ["install"], cd: Path.join(path, "./assets"), into: IO.binstream())
System.cmd("mix", ["assets.build"], cd: path, into: IO.binstream())
defmodule Example.ErrorView do
def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end
defmodule Example.HomeLive do
use Phoenix.LiveView, layout: {__MODULE__, :live}
def mount(_params, _session, socket) do
socket
|> assign(:property, %{foo: 1, bar: 2, baz: 3})
|> assign(:count, 0)
|> start_async(:property, fn ->
Process.sleep(1000)
%{foo: 4, bar: 5, baz: 6}
end)
|> then(&{:ok, &1, temporary_assigns: [property: nil]})
end
def handle_async(:property, {:ok, result}, socket) do
{:noreply, assign(socket, :property, result)}
end
def render("live.html", assigns) do
~H"""
<script src="/assets/phoenix/phoenix.js"></script>
<script src="/assets/phoenix_live_view/phoenix_live_view.js"></script>
<%!-- uncomment to use enable tailwind --%>
<%!-- <script src="https://cdn.tailwindcss.com"></script> --%>
<script>
let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket)
liveSocket.connect()
</script>
<style>
* { font-size: 1.1em; }
</style>
<%= @inner_content %>
"""
end
def render(assigns) do
~H"""
<%= @count %>
<button phx-click="inc">+</button>
<button phx-click="dec">-</button>
<%= @property.foo %>
<%= @property.bar %>
<%= @property.baz %>
<div style="border: 1px solid black">
<.live_component id="lc" module={Example.LiveComponent} />
</div>
"""
end
def handle_event("inc", _params, socket) do
{:noreply, assign(socket, :count, socket.assigns.count + 1)}
end
def handle_event("dec", _params, socket) do
{:noreply, assign(socket, :count, socket.assigns.count - 1)}
end
end
defmodule Example.LiveComponent do
use Phoenix.LiveComponent
def mount(socket) do
socket
|> assign(:property, %{foo: 10, bar: 20, baz: 30})
|> assign(:count, 0)
|> start_async(:property, fn ->
Process.sleep(1000)
%{foo: 40, bar: 50, baz: 60}
end)
|> then(&{:ok, &1, temporary_assigns: [property: nil]})
end
def handle_async(:property, {:ok, result}, socket) do
{:noreply, assign(socket, :property, result)}
end
def render(assigns) do
~H"""
<div>
<%= @count %>
<button phx-click="inc" phx-target={@myself}>+</button>
<button phx-click="dec" phx-target={@myself}>-</button>
<%= @property.foo %>
<%= @property.bar %>
<%= @property.baz %>
</div>
"""
end
def handle_event("inc", _params, socket) do
{:noreply, assign(socket, :count, socket.assigns.count + 1)}
end
def handle_event("dec", _params, socket) do
{:noreply, assign(socket, :count, socket.assigns.count - 1)}
end
end
defmodule Example.Router do
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug(:accepts, ["html"])
end
scope "/", Example do
pipe_through(:browser)
live("/", HomeLive, :index)
end
end
defmodule Example.Endpoint do
use Phoenix.Endpoint, otp_app: :sample
socket("/live", Phoenix.LiveView.Socket)
plug(Plug.Static, from: {:phoenix, "priv/static"}, at: "/assets/phoenix")
plug(Plug.Static, from: {:phoenix_live_view, "priv/static"}, at: "/assets/phoenix_live_view")
plug(Example.Router)
end
{:ok, _} = Supervisor.start_link([Example.Endpoint], strategy: :one_for_one)
Process.sleep(:infinity)
Temporary assigns tell LV to reset the value of the assign to the value given in mount and only re-render the parts of the template using the assign when it is assigned again.