Nested LiveView Child Has Strange ID

Below I inspect the socket in the parent LiveView and the child LiveView mount functions.

In LiveView, mount always runs twice (once before connected, once after connected).

# PARENT (parent mount)
%Phoenix.LiveView.Socket{
  assigns: %{},
  changed: %{},
  connected?: false,
  endpoint: JavelinWeb.Endpoint,
  fingerprints: {nil, %{}},
  id: "phx-k87LXud9",
  parent_pid: nil,
  private: %{
    assigned_new: {%{
       current_user: %Javelin.Users.User{
         __meta__: #Ecto.Schema.Metadata<:built, "users">,
         confirm_password: nil,
         current_password: nil,
         email: nil,
         id: 231,
         inserted_at: nil,
         password: nil,
         password_hash: nil,
         updated_at: nil
       }
     }, []},
    connect_params: %{}
  },
  redirected: nil,
  router: JavelinWeb.Router,
  view: JavelinWeb.ProjectLive
}

# PARENT (child mount)
%Phoenix.LiveView.Socket{
  assigns: %{current_user_id: 231, name: "Pedro Jones", users: []},
  changed: %{current_user_id: true, name: true, users: true},
  connected?: false,
  endpoint: JavelinWeb.Endpoint,
  fingerprints: {nil, %{}},
  id: "phx-k87LXud9",
  parent_pid: nil,
  private: %{
    assigned_new: {%{
       current_user: %Javelin.Users.User{
         __meta__: #Ecto.Schema.Metadata<:built, "users">,
         confirm_password: nil,
         current_password: nil,
         email: nil,
         id: 231,
         inserted_at: nil,
         password: nil,
         password_hash: nil,
         updated_at: nil
       }
     }, []},
    connect_params: %{}
  },
  redirected: nil,
  router: JavelinWeb.Router,
  view: JavelinWeb.ProjectLive
}

# CHILD (child mount)
%Phoenix.LiveView.Socket{
  assigns: %{},
  changed: %{},
  connected?: false,
  endpoint: JavelinWeb.Endpoint,
  fingerprints: {nil, %{}},
  id: "phx-k87LXud9JavelinWeb.DiscoveryCategoriesLive",
  parent_pid: #PID<0.733.0>,
  private: %{
    assigned_new: {%{current_user_id: 231, name: "Pedro Jones", users: []}, []},
    connect_params: %{}
  },
  redirected: nil,
  router: JavelinWeb.Router,
  view: nil
}

[info] Sent 200 in 1ms
[info] CONNECTED TO Phoenix.LiveView.Socket in 211µs
  Transport: :websocket
  Serializer: Phoenix.Socket.V2.JSONSerializer
  Connect Info: %{}
  Parameters: %{"vsn" => "2.0.0"}

## START SECOND GO AROUND
 
# PARENT (parent mount)
%Phoenix.LiveView.Socket{
  assigns: %{},
  changed: %{},
  connected?: true,
  endpoint: JavelinWeb.Endpoint,
  fingerprints: {nil, %{}},
  id: "phx-k87LXud9",
  parent_pid: nil,
  private: %{assigned_new: {%{}, []}, connect_params: %{}},
  redirected: nil,
  router: JavelinWeb.Router,
  view: JavelinWeb.ProjectLive
}

# PARENT (child mount)
%Phoenix.LiveView.Socket{
  assigns: %{},
  changed: %{current_user_id: true, name: true, users: true},
  connected?: true,
  endpoint: JavelinWeb.Endpoint,
  fingerprints: {nil, %{}},
  id: "phx-k87LXud9",
  parent_pid: nil,
  private: %{connect_params: %{}},
  redirected: nil,
  router: JavelinWeb.Router,
  view: JavelinWeb.ProjectLive
}

# CHILD (child mount)
%Phoenix.LiveView.Socket{
  assigns: %{},
  changed: %{},
  connected?: true,
  endpoint: JavelinWeb.Endpoint,
  fingerprints: {nil, %{}},
  id: "phx-k87LXud9JavelinWeb.DiscoveryCategoriesLive",
  parent_pid: #PID<0.753.0>,
  private: %{assigned_new: {%{}, []}, connect_params: %{}},
  redirected: nil,
  router: JavelinWeb.Router,
  view: JavelinWeb.DiscoveryCategoriesLive
}

I noticed two weird things.

  1. The parent socket in the child mount. The first time it changes all the values in assigns to true for some reason. The second time the assigns is empty but again it has the changes with all the values set to true.

  2. The ID of the child socket is basically the same ID as the parent but with the name of the module attached “phx-k87LXud9JavelinWeb.DiscoveryCategoriesLive”

Is this the intended behavior?

Update: I am rendering the child from the parent’s template. Code below.

Parent Render function (Slime Template)

def render(assigns) do
    ~L"""
      <%= JavelinWeb.PageView.render("project.html", assigns) %>
    """
  end

Parent Template (Slime)

= live_render(@socket, JavelinWeb.DiscoveryCategoriesLive, session: %{parent_socket: @socket})

Answer from @chrismccord on the Elixir Slack:

chrismccord [10:12 AM]
@Trevor Owens can you rephrase what you find weird?
the parent and child are two separate processes with two separate socket states
the true values you see aren’t the assigns, it’s our change tracking. See the :changed key in your output
the child id is prefixed with the parent, but its value is an impl detail. What you showed is expected :slightly_smiling_face:

Trevor Owens [10:14 AM]
@chrismccord OK thanks, just making sure it’s what’s expected
this just means you couldn’t render the same child twice, as that would have the same ID? @chrismccord

chrismccord [10:16 AM]
you have to pass a :child_id a to live_render if rendering dup children (edited)

Trevor Owens [10:16 AM]
I’m just getting my feet wet with nesting LiveViews, and trying to make each child like a “component”
got it
I actually just saw that

chrismccord [10:17 AM]
we are supposed to raise on dup children and tell you to use a child_id option, but that stopped raising a few commits back. There is an issue tracking that regression

Trevor Owens [10:17 AM]
link to github issue
this showed me there’s quite a bit of options you can pass in the live_render function

chrismccord [10:19 AM]
keep in mind nested children should be used for the same reasons you would use a separate process, namely:

  1. you want isolation from failures
  2. you want concurrency
    if those don’t apply, you should use a regular render instead with a “static” leex template

Trevor Owens [10:20 AM]
got it

chrismccord [10:20 AM]
the only caveat right now is the parent receives the events, which you would need to delegate to your “component” module if you wanted to keep code separated. I will explore event delegation in the future to a static module, but you can proxy now if needed

Trevor Owens [10:21 AM]
I am doing multi-step forms with interactive editable tables
so the parent manages where the multi-step form is
and the editable tables manage their own state
there are also two different types of editable tables depending on what the user chooses
the multi-step forms are multi-player
used in live events/workshops
Will see if I can refactor to remove the parent and just make the child a partial inside the static parent template, then have the child use redirects to other LiveViews. Are there advantages to that?

3 Likes