phoenixframework / phoenix_live_view

Rich, real-time user experiences with server-rendered HTML
https://hex.pm/packages/phoenix_live_view
MIT License
6.14k stars 919 forks source link

Clarify that using assign/3 will not break change tracking #3294

Closed davydog187 closed 3 months ago

davydog187 commented 3 months ago

I was under the impression that using assign/3 and friends inside of a function would break change tracking, which is not the case.

A quick note to clarify this behavior

SteffenDE commented 3 months ago

Maybe we'll need to go a little deeper here, because simply stating that it maintains change tracking may lead to wrong assumptions. While it is true that using assign/3 etc. in a function maintains change tracking, it only maintains it in the sense that nothing breaks. But the items that are assigned are always considered to be changed and therefore always sent over the wire. So if you do something like

def my_component(assigns) do
  assigns = assign(assigns, :foo, assigns.a + assigns.b)

  ~H"""
  <%= @foo %>
  """
end

This will always send the diff for foo, even if a and b did not change. I feel like this was the intention behind the line

Instead explicitly precompute the assign outside of render

which was already changed in https://github.com/phoenixframework/phoenix_live_view/commit/cea2ab233cc34ce7a3b2659ebf7423909e691347 because people misunderstood it that assign/3 is not possible in render, but maybe it's kind of worse now in that regard.

In function components the "assign at the top of the function" pattern is often used from what I've seen. I'm not sure if I'd go that far, but one could currently call it an anti pattern for diff size over the wire.

Opinions? @chrismccord @josevalim

(Maybe an assign/3 macro could be more intelligent in such cases?)

davydog187 commented 3 months ago

@SteffenDE you are correct. The point I wanted to clarify is that the surrounding template will not be resent, even if the variable @foo has.

Consider the following LiveView

defmodule MyAppWeb.TestLive do

  use MyAppWeb, :live_view

  def mount(_, _, socket) do
    if connected?(socket) do
      send(self(), :tick)
    end

    {:ok, socket |> assign_random()}
  end

  def handle_info(:tick, socket) do
    Process.send_after(self(), :tick, :timer.seconds(1))
    {:noreply, assign_random(socket)}
  end

  defp assign_random(socket) do
    assign(socket, x: Enum.random(1..1000), y: Enum.random(1..1000))
  end

  def show_math(assigns) do
    assigns = assign(assigns, :sum, assigns.x + assigns.y)

    ~H"""
    <div class="border p-4">
      <span class="text-bold"><%= @x %></span>
      <span class="text-bold">+</span>
      <span class="text-bold"><%= @y %></span>
      <span class="text-bold"><%= @sum %></span>
    </div>
    """
  end

  def render(assigns) do
    ~H"""
    <h1>Let's do math!</h1>

    <.show_math x={@x} y={@y} />
    """
  end
end

After the initial render, only the changes to @x, @y and @sum are sent over the wire, which was not my mental model for using assigns/3. The point I want to clarify for readers is that you don't need to worry about the static parts of the template changing.

This might mean that change tracking might need a clearer definition, with possibly more examples of how it degrades in certain scenarios. I am not alone in being confused about the behavior here

josevalim commented 3 months ago

I pushed a version as a comment, let me know what you think.

davydog187 commented 3 months ago

I have an idea to clarify further, will push something later. Thanks @josevalim and @SteffenDE

davydog187 commented 3 months ago

@josevalim take another look. The main thing I think that the docs were missing is exactly how the rendering degrades in certain failure scenarios.

Particularly, by only using a single variable @sum, it was hard to illustrate how using variables leads to bad templates. I hope this is a good tradeoff between clarity and understanding

josevalim commented 3 months ago

:green_heart: :blue_heart: :purple_heart: :yellow_heart: :heart: