Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add update_many/1 to LiveComponent #2809

Merged
merged 3 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 54 additions & 62 deletions lib/phoenix_live_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ defmodule Phoenix.LiveComponent do

### Mount and update

Stateful components are identified by the component module and their ID.
Therefore, two stateful components with the same module and ID are treated
Live components are identified by the component module and their ID.
Therefore, two live components with the same module and ID are treated
as the same component. We often tie the component ID to some application based ID:

<.live_component module={UserComponent} id={@user.id} user={@user} />
Expand Down Expand Up @@ -85,7 +85,7 @@ defmodule Phoenix.LiveComponent do

### Events

Stateful components can also implement the `c:handle_event/3` callback
LiveComponents can also implement the `c:handle_event/3` callback
that works exactly the same as in LiveView. For a client event to
reach a component, the tag must be annotated with a `phx-target`.
If you want to send the event to yourself, you can simply use the
Expand Down Expand Up @@ -121,23 +121,15 @@ defmodule Phoenix.LiveComponent do
Dismiss
</a>

### Preloading and update
### Update many

Stateful components also support an optional `c:preload/1` callback.
The `c:preload/1` callback is useful when multiple components of the
same type are rendered on the page and you want to preload or augment
their data in batches.

Once a LiveView renders a LiveComponent, the optional `c:preload/1` and
`c:update/2` callbacks are called before `c:render/1`.

So on first render, the following callbacks will be invoked:

preload(list_of_assigns) -> mount(socket) -> update(assigns, socket) -> render(assigns)

On subsequent renders, these callbacks will be invoked:

preload(list_of_assigns) -> update(assigns, socket) -> render(assigns)
Live components also support an optional `c:update_many/1` callback
as an alternative to `c:update/2`. While `c:update/2` is called for
each component individially, `c:update_many/1` is called with all
LiveComponents of the same module being currently rendered/updated.
The advantage is that you can preload data from the database using
a single query for all components, instead of running one query per
component.

To provide a more complete understanding of why both callbacks are necessary,
let's see an example. Imagine you are implementing a component and the component
Expand All @@ -155,30 +147,30 @@ defmodule Phoenix.LiveComponent do

However, the issue with said approach is that, if you are rendering
multiple user components in the same page, you have a N+1 query problem.
The `c:preload/1` callback helps address this problem as it is invoked
with a list of assigns for all components of the same type. For example,
instead of implementing `c:update/2` as above, one could implement:
By using `c:update_many/1` instead of `c:update/2` , we receive a list
of all assigns and sockets, allowing us to update many at once:

def preload(list_of_assigns) do
list_of_ids = Enum.map(list_of_assigns, & &1.id)
def update_many(assigns_sockets) do
list_of_ids = Enum.map(assigns_sockets, fn {assigns, _sockets} -> assigns.id end)

users =
from(u in User, where: u.id in ^list_of_ids, select: {u.id, u})
|> Repo.all()
|> Map.new()

Enum.map(list_of_assigns, fn assigns ->
Map.put(assigns, :user, users[assigns.id])
Enum.map(assigns_sockets, fn {assigns, sockets} ->
assign(socket, :user, users[assigns.id])
end)
end

Now only a single query to the database will be made. In fact, the
preloading algorithm is a breadth-first tree traversal, which means
`update_many/2` algorithm is a breadth-first tree traversal, which means
that even for nested components, the amount of queries are kept to
a minimum.

Finally, note that `c:preload/1` must return an updated `list_of_assigns`,
keeping the assigns in the same order as they were given.
Finally, note that `c:update_many/1` must return an updated list of
sockets in the same order as they are given. If `c:update_many/1` is
defined, `c:update/2` is not invoked.

### Summary

Expand All @@ -189,36 +181,38 @@ defmodule Phoenix.LiveComponent do

```mermaid
flowchart LR
*((start)):::event-.->P
WE([wait for<br>parent changes]):::event-.->P
W([wait for<br>events]):::event-.->H
*((start)):::event-.->M
WE([wait for<br>parent changes]):::event-.->M
W([wait for<br>events]):::event-.->H

subgraph j__transparent[" "]
subgraph j__transparent[" "]

subgraph i[" "]
direction TB
P(preload/1):::callback-->M(mount/1)
M(mount/1<br><em>only once</em>):::callback-->U
end
subgraph i[" "]
direction TB
M(mount/1<br><em>only once</em>):::callback
M-->U
M-->UM
end

U(update/2):::callback-->A
U(update/2):::callback-->A
UM(update_many/1):::callback-->A

subgraph j[" "]
direction TB
A --> |yes| R
H(handle_event/3):::callback-->A{any<br>changes?}:::diamond
end
subgraph j[" "]
direction TB
A --> |yes| R
H(handle_event/3):::callback-->A{any<br>changes?}:::diamond
end

A --> |no| W
A --> |no| W

end
end

R(render/1):::callback_req-->W
R(render/1):::callback_req-->W

classDef event fill:#fff,color:#000,stroke:#000
classDef diamond fill:#FFC28C,color:#000,stroke:#000
classDef callback fill:#B7ADFF,color:#000,stroke-width:0
classDef callback_req fill:#B7ADFF,color:#000,stroke-width:0,text-decoration:underline
classDef event fill:#fff,color:#000,stroke:#000
classDef diamond fill:#FFC28C,color:#000,stroke:#000
classDef callback fill:#B7ADFF,color:#000,stroke-width:0
classDef callback_req fill:#B7ADFF,color:#000,stroke-width:0,text-decoration:underline
```

## Slots
Expand Down Expand Up @@ -252,7 +246,7 @@ defmodule Phoenix.LiveComponent do
the two different approaches in detail.

Imagine a scenario where a LiveView represents a board with each card
in it as a separate stateful LiveComponent. Each card has a form to
in it as a separate LiveComponent. Each card has a form to
allow update of the card title directly in the component, as follows:

defmodule CardComponent do
Expand Down Expand Up @@ -375,9 +369,9 @@ defmodule Phoenix.LiveComponent do
will be invoked, triggering both preload and update callbacks, which will
load the most up to date data from the database.

## Cost of stateful components
## Cost of live components

The internal infrastructure LiveView uses to keep track of stateful
The internal infrastructure LiveView uses to keep track of live
components is very lightweight. However, be aware that in order to
provide change tracking and to send diffs over the wire, all of the
components' assigns are kept in memory - exactly as it is done in
Expand All @@ -398,7 +392,7 @@ defmodule Phoenix.LiveComponent do
in the code above, the view and the component will share the same copies
of the `@user` and `@org` assigns.

You should also avoid using stateful components to provide abstract DOM
You should also avoid using live components to provide abstract DOM
components. As a guideline, a good LiveComponent encapsulates
application concerns and not DOM functionality. For example, if you
have a page that shows products for sale, you can encapsulate the
Expand Down Expand Up @@ -436,7 +430,7 @@ defmodule Phoenix.LiveComponent do

If you keep components mostly as an application concern with
only the necessary assigns, it is unlikely you will run into
issues related to stateful components.
issues related to live components.

## Limitations

Expand Down Expand Up @@ -470,7 +464,7 @@ defmodule Phoenix.LiveComponent do
defmodule CID do
@moduledoc """
The struct representing an internal unique reference to the component instance,
available as the `@myself` assign in stateful components.
available as the `@myself` assign in live components.

Read more about the uses of `@myself` in the `Phoenix.LiveComponent` docs.
"""
Expand Down Expand Up @@ -515,11 +509,9 @@ defmodule Phoenix.LiveComponent do
@callback mount(socket :: Socket.t()) ::
{:ok, Socket.t()} | {:ok, Socket.t(), keyword()}

@callback preload(list_of_assigns :: [Socket.assigns()]) ::
list_of_assigns :: [Socket.assigns()]
@callback update(assigns :: Socket.assigns(), socket :: Socket.t()) :: {:ok, Socket.t()}

@callback update(assigns :: Socket.assigns(), socket :: Socket.t()) ::
{:ok, Socket.t()}
@callback update_many([{Socket.assigns(), Socket.t()}]) :: [Socket.t()]

@callback render(assigns :: Socket.assigns()) :: Phoenix.LiveView.Rendered.t()

Expand All @@ -530,5 +522,5 @@ defmodule Phoenix.LiveComponent do
) ::
{:noreply, Socket.t()} | {:reply, map, Socket.t()}

@optional_callbacks mount: 1, preload: 1, update: 2, handle_event: 3
@optional_callbacks mount: 1, update_many: 1, update: 2, handle_event: 3
end
91 changes: 65 additions & 26 deletions lib/phoenix_live_view/diff.ex
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ defmodule Phoenix.LiveView.Diff do
render_component(csocket, component, id, cid, false, %{}, cids, %{}, components)

{cdiffs, components} =
render_pending_components(socket, pending, %{}, cids, cdiffs, components)
render_pending_components(socket, pending, cids, cdiffs, components)

{diff, cdiffs} = extract_events({diff, cdiffs})
{Map.put(diff, @components, cdiffs), components, extra}
Expand Down Expand Up @@ -627,44 +627,82 @@ defmodule Phoenix.LiveView.Diff do

{{pending, diffs, components}, seen_ids} =
Enum.reduce(pending, acc, fn {component, entries}, acc ->
{{pending, diffs, components}, seen_ids} = acc
update_many? = function_exported?(component, :update_many, 1)
entries = maybe_preload_components(component, Enum.reverse(entries))

Enum.reduce(entries, acc, fn {cid, id, new?, new_assigns}, {triplet, seen_ids} ->
{pending, diffs, components} = triplet

if Map.has_key?(seen_ids, [component | id]) do
raise "found duplicate ID #{inspect(id)} " <>
"for component #{inspect(component)} when rendering template"
end
{assigns_sockets, metadata, components, seen_ids} =
Enum.reduce(entries, {[], [], components, seen_ids}, fn
{cid, id, new?, new_assigns}, {assigns_sockets, metadata, components, seen_ids} ->
if Map.has_key?(seen_ids, [component | id]) do
raise "found duplicate ID #{inspect(id)} " <>
"for component #{inspect(component)} when rendering template"
end

{socket, components} =
case cids do
%{^cid => {_component, _id, assigns, private, prints}} ->
private = Map.delete(private, @marked_for_deletion)
{configure_socket_for_component(socket, assigns, private, prints), components}
{socket, components} =
case cids do
%{^cid => {_component, _id, assigns, private, prints}} ->
private = Map.delete(private, @marked_for_deletion)
{configure_socket_for_component(socket, assigns, private, prints), components}

%{} ->
myself_assigns = %{myself: %Phoenix.LiveComponent.CID{cid: cid}}

{mount_component(socket, component, myself_assigns),
put_cid(components, component, id, cid)}
end

assigns_sockets =
if update_many? do
[{new_assigns, socket} | assigns_sockets]
else
[Utils.maybe_call_update!(socket, component, new_assigns) | assigns_sockets]
end

metadata = [{cid, id, new?} | metadata]
seen_ids = Map.put(seen_ids, [component | id], true)
{assigns_sockets, metadata, components, seen_ids}
end)

%{} ->
myself_assigns = %{myself: %Phoenix.LiveComponent.CID{cid: cid}}
sockets =
if update_many? do
component.update_many(Enum.reverse(assigns_sockets))
else
Enum.reverse(assigns_sockets)
end

{mount_component(socket, component, myself_assigns),
put_cid(components, component, id, cid)}
end
metadata = Enum.reverse(metadata)
triplet = zip_components(sockets, metadata, component, cids, {pending, diffs, components})
{triplet, seen_ids}
end)

socket = Utils.maybe_call_update!(socket, component, new_assigns)
diffs = maybe_put_events(diffs, socket)
render_pending_components(socket, pending, seen_ids, cids, diffs, components)
end

triplet =
render_component(socket, component, id, cid, new?, pending, cids, diffs, components)
defp zip_components(
[%{__struct__: Phoenix.LiveView.Socket} = socket | sockets],
[{cid, id, new?} | metadata],
component,
cids,
{pending, diffs, components}
) do
diffs = maybe_put_events(diffs, socket)
acc = render_component(socket, component, id, cid, new?, pending, cids, diffs, components)
zip_components(sockets, metadata, component, cids, acc)
end

{triplet, Map.put(seen_ids, [component | id], true)}
end)
end)
defp zip_components([], [], _component, _cids, acc) do
acc
end

render_pending_components(socket, pending, seen_ids, cids, diffs, components)
defp zip_components(_sockets, _metadata, component, _cids, _acc) do
raise "#{inspect(component)}.update_many/1 must return a list of Phoenix.LiveView.Socket " <>
"of the same length as the input list, got mismatched return type"
end

defp maybe_preload_components(component, entries) do
if function_exported?(component, :preload, 1) do
IO.warn("#{inspect(component)}.preload/1 is deprecated, use update_many/1 instead")
list_of_assigns = Enum.map(entries, fn {_cid, _id, _new?, new_assigns} -> new_assigns end)
result = component.preload(list_of_assigns)
zip_preloads(result, entries, component, result)
Expand All @@ -675,6 +713,7 @@ defmodule Phoenix.LiveView.Diff do

defp maybe_call_preload!(module, assigns) do
if function_exported?(module, :preload, 1) do
IO.warn("#{inspect(module)}.preload/1 is deprecated, use update_many/1 instead")
[new_assigns] = module.preload([assigns])
new_assigns
else
Expand Down
Loading