sezaru

sezaru

Proposal: Support "memory releasable" assigns

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.

Most Liked

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

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).

Where Next?

Popular in Proposals: Ideas Top

cevado
I was reading the EEP-79, and thinking about the poor record support in Elixir(i’ve tried to discuss about that in the forum before). I s...
New
andreamancuso
Hey folks, This might sound niche, but I think it’s worth bringing up - especially given Phoenix’s reputation for being lightweight, por...
New
marcandre
I notice that most events have bindings (e.g. phx-keyup) but not the input event. The input event is the preferred way to interact with ...
New
dkuku
This is a proposal to make the map key mismatch errors a bit better: Every time I have a typo It’s very challenging for me even when I u...
New
zachdaniel
Something we do in various Ash packages is provide routes that the user forwards to in their own application. This is because we are deri...
New
dimitarvp
To @jonatanklosko and @the-mikedavis: I see that there is a Rust crate at crates.io: Rust Package Registry but it is pointing at https:/...
New
altdsoy
Hi there :wave:, I hope everyone is doing well… Tl;Dr: Is there a use case to have Heroicons from an actual hex.pm package instead of G...
New
dibok
Hi, I’m trying to use phoenix.js in my Qt QML project which has it’s own buildin JavaScript engine. Problem is that (what I googled so f...
New
sodapopcan
This past weekend I’ve been doing some work on a PR to bring open_browser to PhoenixTest. For those who don’t know it, PhoenixTest is a ...
New
alaadahmed
Hi folks, I tried Phoenix 1.7 and it is awesome, but I have small suggestion, which in my opinion will organize files in a better way. ...
New

Other popular topics Top

axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 48342 226
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
AstonJ
Please see the new poll here: Which code editor or IDE do you use? (Poll) (2022 Edition) It’s been a while since we first asked this, I...
208 31265 143
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36352 110
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 127089 1222
New

We're in Beta

About us Mission Statement