Proposal - mount/2 for Phoenix.LiveComponent

506 views
Skip to first unread message

Ingmar de Lange

unread,
Jun 16, 2020, 8:02:34 AM6/16/20
to phoenix-core
Currently when composing nested LiveComponents child components do not have access to "parent" assigns.
Only when parent assigns are copied into child assigns do children have access and only in update/2, not while mounting.
For me it would be very convenient to be able to read contextual data while mounting a stateful child component.
It would enable me to initialize an independent child state derived from "parent" state once while mounting.

Function render_pending_components/5 in Phoenix.LiveView.Diff could pass contextual data to mount_component/3.

{socket, components} =
  case cid_to_component do
    %{^cid => {_component, _id, assigns, private, prints}} ->
      {configure_socket_for_component(socket, assigns, private, prints), components}

    %{} ->
      {mount_component(socket, component, %{myself: cid}, cid_to_component),
        put_cid(components, component, id, cid)}
  end

Then mount_component/3 would become mount_component/4.

defp mount_component(socket, component, assigns, context \\ %{}) do
  socket =
    configure_socket_for_component(
      socket,
      assigns,
      Map.take(socket.private, [:conn_session]),
      new_fingerprints()
    )
    |> Utils.assign(:flash, %{})

  Utils.maybe_call_mount!(socket, component, [context, socket])
end


Only when the client is interested in receiving contextual data would Phoenix.LiveView.Utils format the data and pass it to a mount function.

def maybe_call_mount!(socket, view, args) do
  # arity = length(args)

  {arity, args} =
    case length(args) do
      2  -> if function_exported?(view, :mount, 2) do
              {2, format_context(args)}
            else
              {1, tl(args)}  # drop context from args
            end
      n  -> {n, args}
    end


Possible formatting.

defp format_context([context, socket]) do
  formatted =
    Enum.reduce(context, %{}, fn {_, {component, id, assigns, _private, _prints}}, acc ->
      Map.put(acc, id, {component, assigns})
    end)

  [formatted, socket]
end

Mount function in LiveComponent.

def mount(context, socket) do
  # assign independent child state derived from context
  {:ok, socket}
end


I am hoping something like this could be considered.

Cheers,
Ingmar



José Valim

unread,
Jun 16, 2020, 8:47:18 AM6/16/20
to phoeni...@googlegroups.com
Yeah, this probably makes sense. I have been guarding my update/2 function on something like this:

if socket.assigns[:computed_assign] do
  {:ok, socket}
else
  {:ok, socket |> assign(..., ...)}
end

On the positive side, if you have a single field, assign_new works just fine.

--
You received this message because you are subscribed to the Google Groups "phoenix-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to phoenix-core...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/phoenix-core/6228575c-30ea-4df8-8d29-a85c7b3e53b6o%40googlegroups.com.

Ingmar

unread,
Jun 16, 2020, 11:23:37 AM6/16/20
to phoeni...@googlegroups.com
For me benefits would be not having to create many unnecessary copies of the same state, avoid stacks of states and being able to use the mount function to initialize state.
Also, like you said, not having to pattern match on some computed state in order to initialize a LiveComponent would be great.

My previous proposal did not include LiveView state.
So for toplevel LiveComponents the context would always be an empty map.
I would also like to access the LiveView state, so maybe something like this would be better.

{socket, components} =
  case cid_to_component do
    %{^cid => {_component, _id, assigns, private, prints}} ->
      {configure_socket_for_component(socket, assigns, private, prints), components}

    %{} ->
      context = Map.put(
        cid_to_component, "root", {socket.view, :root, socket.assigns, nil, nil}
      )

      {mount_component(socket, component, %{myself: cid}, context),

        put_cid(components, component, id, cid)}
  end

Cheers,
Ingmar



Op di 16 jun. 2020 om 14:47 schreef José Valim <jose....@dashbit.co>:

José Valim

unread,
Jun 16, 2020, 11:27:03 AM6/16/20
to phoeni...@googlegroups.com
> For me benefits would be not having to create many unnecessary copies of the same state, avoid stacks of states and being able to use the mount function to initialize state.

Can you clarify? I don't see how mount is any better than updates when it comes to copies of the same state / stacks of states.

Ingmar

unread,
Jun 16, 2020, 11:39:21 AM6/16/20
to phoeni...@googlegroups.com
That's because assigns are kept in memory, so when you pass an assign from parent to child by assigning to the child the assign is copied into the child state (while that child may not need that assign to be in it's state).
If the child could just read the state then the state wouldn't have to be copied, it would only be a function argument to mount/2.
With regard to stacks I mean toplevel component A state + child component B state copied into child component C state.


Op di 16 jun. 2020 om 17:27 schreef José Valim <jose....@dashbit.co>:

José Valim

unread,
Jun 16, 2020, 11:43:56 AM6/16/20
to phoeni...@googlegroups.com
That's not quite true. Components are in the same process. There is no deep copying going on. You may add new keys, but they all point to the same value on merge. And you also have control over what to merge or not on update.

Ingmar

unread,
Jun 16, 2020, 12:02:17 PM6/16/20
to phoeni...@googlegroups.com
> but they all point to the same value on merge

To be honest I am not exactly sure what that means.
I will try to be more clear about what I mean.

Suppose you have stateful nested LiveComponents like this:
A > B > C > D

Where A is a toplevel LiveComponent rendered by the LiveView.

Suppose D needs to know about some setting in A.
Then you would need to assign that setting to B (which I think is a copy because it's a new key in B),
and then from B to C and from C to D.
So in the end you would have 4 copies of that setting so that D can read it.
The copying is a result of assigning which is the only way to get the setting all the way to D.
Also you would have to assign things to B and C while they are not interested in that setting.


Op di 16 jun. 2020 om 17:43 schreef José Valim <jose....@dashbit.co>:

José Valim

unread,
Jun 16, 2020, 12:09:44 PM6/16/20
to phoeni...@googlegroups.com
Now I understand what you mean. You literally want to pass the "parent" LiveView assigns and bypass all of the components along the way. I don't think that's a good idea. What if the LiveView state changes? You may have an outdated component. This can even have security implications. If D needs an assign, then B and C definitely have to worry about it.

In any case, my point is that there is way less copying involved in these operations than one would expect. :) They are all in the same process, passing data between components do not incur any copying.


Ingmar

unread,
Jun 16, 2020, 1:01:20 PM6/16/20
to phoeni...@googlegroups.com
I agree with you that B and C need to worry about it, so I regret that part of the example.
State changes are not (necessarily) a problem when the "parent" state or "context" I'm talking about is passed to the mount function in the LiveComponent.
At that point in time it is the correct state and the parent decided the child should be rendered (initialized).
The parent should worry about the child becoming outdated by keeping an eye on relevant state changes and delete the child if necessary.
When the child is rendered again it is also mounted again with the correct (initial) state.

An example use case could be a child component that renders a form and reports back when it's done.
Both parent and child component may have a setting foobar.
The child component only wants to know about the current foobar setting on mount.
The parent foobar setting should not be changed when the child foobar setting changes.
The child state may be completely independent.
Only when the child is done and reports back should the parent update itself.
So the parent foobar setting is really just a readonly parameter that is used to initialize the child.

This is a very simple example that does not express the need for contextual data very well (assigns will do fine here).
The need for easily accessible contextual data becomes more apparent when the (nested) components and their state become more complex.
Some settings are needed by many components and may not change a lot, like for example timezone, language, user id, etc.
It would be a lot easier when those values would be available on mount without having to pass them along by assigning.

I understand you are worried about bad design choices and it's true that it's a handle with care feature.

Maybe someday I will fully understand how you guys managed to build this :)


Op di 16 jun. 2020 om 18:09 schreef José Valim <jose....@dashbit.co>:
Reply all
Reply to author
Forward
0 new messages