LiveView render hook for derived assigns - suggested approaches?

Hey, just getting started with Phoenix + LiveView, coming from Django and React. I’m a bit overwhelmed by the various ways of approaching templating, so apologies in advance if there’s an obvious solution here.

What I’m looking for is the ability to hook into a LiveView render() call, such that I can calculate derived assigns to pass to the template (i.e. so that I can modify assigns). I’d like to do this to keep my LiveView state small and shaped like an entity store, and derive template usage in the render method, as in React.

I understand that function components support this, when leveraging the ~H sigil, but I’m looking to keep my template separate from the LiveView. As an example, the auto generated core components:

  def flash(assigns) do
    assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)

    ~H"""
    ... template here

What I’m looking to do:

  @impl true
  def render(assigns) do
    assigns = derive_view_model(assigns)

    actual_render("template.html.heex", assigns)
  end

As I understand, LiveView automatically generates a render() function for the template of the same module name. What I’d like to do is understand how to reproduce that method so that I can customize it, or otherwise hook into it.

My current approach is to hook into assign() calls manually - e.g. socket |> delete_entity(:alerts, alert) |> derive_view_model() . However even in my first LiveView page I have forgotten to do this several times, so I’m hoping there’s a better way.

I have a function called set_state in pretty much every LiveView which basically does this, yes you have to manually call it where needed. But it’s pretty flexible, you could have different ones depending on how expensive they are, and only call them when needed. But there is no automatic way of doing it.

You can use attach_hook/4 to run a function after handle_params, handle_event and handle_info. This will ensure that these derived assigns are set all the time. Use on_mount for hooking into mount

Reading the docs, I see that hooks are run before the handle_foo handlers:

Lifecycle hooks take place immediately before a given lifecycle callback is invoked on the LiveView

This would not work for my use case, since I’d be looking to update the derived assigns after the handle_foo methods have run. Eg this code:

  @impl true
  def handle_info({:stock_patch, stock}, socket) do
    {:noreply, socket |> patch_entity(:stocks, stock) |> derive_view_model()}
  end

I’d need to be able to run a hook post-handler, with the updated assigns, so that the view model may be calculated correctly.

If I’m missing something, please let me know!

Thanks for the reply - I’m glad I’m not the only one. It stands out to me that it is possible to accomplish this by only leveraging function components (including for render()) - for consistency, it’d be great to be able to update assigns for template file rendering, too.

I have a function called set_state in pretty much every LiveView which basically does this, yes you have to manually call it where needed. But it’s pretty flexible, you could have different ones depending on how expensive they are, and only call them when needed. But there is no automatic way of doing it.

What you can do, and that’s similar to Emacs’ advice system, is to use the socket as a container for assign hooks but you need to redefine assign. Something like the following:

# The second argument is when the hook is called.
# Could be before, after, around and so on.
advice_assign(socket, :after, &derive_assigns/1)

defp derive_assigns(%{assigns: %{...}} = socket) do
  # Use Phoenix.Component.assign/2,3 here
end

defp assign(%{private: %{assign_hooks: hooks}} = socket) do
  # Process the hooks accordingly.
end

You could do something similar to what Surface does with quoted_mount. Just move the super call to run before you do the state handling:

defmacro before_compile(env) do
quoted_mount(env)
end

defp quoted_mount(env) do
defaults = env.module |> Surface.API.get_defaults() |> Macro.escape()

if Module.defines?(env.module, {:mount, 3}) do
  quote do
    defoverridable mount: 3

    def mount(params, session, socket) do
      socket =
        socket
        |> Surface.init()
        |> assign(unquote(defaults))

      super(params, session, socket)
    end
  end
else
  quote do
    def mount(_params, _session, socket) do
      {:ok,
       socket
       |> Surface.init()
       |> assign(unquote(defaults))}
    end
  end
end

end

sorry for the formatting, I am on mobile now

In LiveView “assigns” are state stored in the server.

The bare building blocks you have are a module and functions (thankfully only two concepts).

The LiveView “as a whole” (~page) is a module, all logic goes into functions.

The framework will call callbacks that you implement on your module: mount, handle_*, etc.

The way I approach derived assigns is computing them as a function of their dependencies (often a private function defp inside the module and called as appropriate).

You can as well inline your templates in your LiveView module, def render(assigns), and then you can do as you wanted/saw in function components as in the CoreComponents module.

Note you need to be careful to follow the rules with regards to change tracking: common pitfalls.

Thanks for all the suggestions. Coming from a React background, I feel most comfortable with @rhcarvalho ‘s approach to inline templates. In React, I’d map liveview assigns to component state , with the render() method in each supporting derived state.