What on earth is going on with my live component assigns? Empty?

I’m trying to render a live component with assigns but they’re not getting merged into the socket of the live component.

This is the liveview and template passing in profile. I have verified that profile is indeed an assign in the parent liveview.

defmodule HydroplaneWeb.OnboardLive do
  use HydroplaneWeb, :live_view

  alias Hydroplane.Users
  alias Hydroplane.Users.Profile

  alias HydroplaneWeb.Onboard.ProfileForm
  alias HydroplaneWeb.Onboard.OrganizationForm

  def mount(_params, _session, socket) do
    {:ok,
     socket
     |> assign_profile()}
  end

  defp assign_profile(%{assigns: %{current_account: current_account}} = socket) do
    profile =
      case Users.get_profile_for_account(current_account) do
        nil -> %Profile{}
        profile -> profile
      end

    assign(socket, :profile, profile)
  end
end
<div class="flex items-center justify-center h-screen">
  <div class="bg-white w-full max-w-md flex flex-col shadow rounded-lg p-4 text-slate-900">
    <h1 class="text-3xl text-slate-800 text-center font-bold">We're glad you can join us!</h1>
    <p class="mt-3 mb-3 text-center">There are just a few things you need to take care of in order to make the best use of Hydroplane.</p>
    <.live_component module={ProfileForm} id="profile-form" profile={@profile} />
  </div>
</div>

And this is where I am trying to access profile but I’m getting an empty assigns with no profile key. Because of this, I’m getting a no function clause matching error.

defmodule HydroplaneWeb.Onboard.ProfileForm do
  use HydroplaneWeb, :live_component

  alias Hydroplane.Users
  alias Hydroplane.Users.Profile

  def mount(socket) do
    {:ok, assign_changeset(socket)}
  end

  defp assign_changeset(%{assigns: %{profile: profile}} = socket) do
    assign(socket, :changeset, Profile.changeset(profile))
  end
end

@lelliott mount/1 is not passed the assigns of the parent. You need to pass all assigns the component will use to it as params explicitly, and then use the update/2 callback on the component to set any assigns on that component based on those params.

The documentation says this:

When live_component/1 is called, mount/1 is called once, when the component is first added to the page. mount/1 receives the socket as argument. Then update/2 is invoked with all of the assigns given to live_component/1. If update/2 is not defined all assigns are simply merged into the socket. After the component is updated, render/1 is called with all assigns.

This gives me the impression that the assigns should be automatically merged in. If this is not the case, it could be a very misleading part of the documentation.

When I render the live component, I’m explicitly passing a profile assign:
<.live_component module={ProfileForm} id="profile-form" profile={@profile} />

Since there is no update defined in the component, the assigns should be automatically merged in.

Ah I see the confusion.

We’re both right, but the sequence is important. “If update/2 is not defined all assigns are simply merged into the socket” happens at the point in time update/2 is called, which is after mount/1.

So you can’t assign_changeset/1 in mount because mount is called before update, and it isn’t until update/2 is called (either a custom one or the default one) that you get the passed assigns.

Oh I understand, though that is odd behaviour imo. Where is the best place to assign_changeset/1 given this constraint?

Or would it be a better practice to build a changeset in the parent liveview and pass that through to the form?

That’s exactly what update/2 is for:

def update(assigns, socket) do
  socket =
    socket
    |> assign(socket, assigns)
    |> assign_changeset()

  {:ok, socket}
end
2 Likes