sezaru

sezaru OP

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.

Any suggestions and improvements are welcome.

First 6 of 6 Posts Switch mode

sodapopcan

sodapopcan

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.

sezaru

sezaru OP

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.

sodapopcan

sodapopcan

Ohhhhh fair enough. Ya this would indeed be nice (although I clearly never run into it much).

steffend

steffend

Phoenix Core Team

This is not correct. In fact temporary_assigns is exactly what you are proposing here :smiley:

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.

sezaru

sezaru OP

You are totally correct! I just tested it myself with your code and it worked great, now I feel dumb to not realizing that myself :sweat_smile:

sodapopcan

sodapopcan

Ha, I also am feeling dumb :poop:

— All posts loaded —

Where Next? Top

Trending in Proposals: Ideas Top

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New

We're in Beta

About us Mission Statement