Skip to content

Commit

Permalink
Deprecate preload and add docs
Browse files Browse the repository at this point in the history
  • Loading branch information
josevalim committed Sep 20, 2023
1 parent d587dc1 commit 58df0a6
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 65 deletions.
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
18 changes: 15 additions & 3 deletions lib/phoenix_live_view/diff.ex
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,18 @@ defmodule Phoenix.LiveView.Diff do

{diff, new_components, :noop} =
write_component(socket, cid, components, fn component_socket, component ->
{Utils.maybe_call_update!(component_socket, component, updated_assigns), :noop}
if function_exported?(component, :update_many, 1) do
case component.update_many([{updated_assigns, component_socket}]) do
[%{__struct__: Phoenix.LiveView.Socket} = socket] ->
{socket, :noop}

other ->
raise "#{inspect(component)}.update_many/1 must return a list of Phoenix.LiveView.Socket " <>
"of the same length as the input list, got: #{inspect(other)}"
end
else
{Utils.maybe_call_update!(component_socket, component, updated_assigns), :noop}
end
end)

{diff, new_components}
Expand Down Expand Up @@ -628,8 +639,7 @@ defmodule Phoenix.LiveView.Diff do
{{pending, diffs, components}, seen_ids} =
Enum.reduce(pending, acc, fn {component, entries}, acc ->
{{pending, diffs, components}, seen_ids} = acc
# TODO: Check for update_many? instead
update_many? = false
update_many? = function_exported?(component, :update_many, 1)
entries = maybe_preload_components(component, Enum.reverse(entries))

{assigns_sockets, metadata, components, seen_ids} =
Expand Down Expand Up @@ -710,6 +720,7 @@ defmodule Phoenix.LiveView.Diff do

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 @@ -720,6 +731,7 @@ defmodule Phoenix.LiveView.Diff do

defp maybe_call_preload!(module, assigns) do
if function_exported?(module, :preload, 1) do
IO.warn("#{inspect(component)}.preload/1 is deprecated, use update_many/1 instead")

Check warning on line 734 in lib/phoenix_live_view/diff.ex

View workflow job for this annotation

GitHub Actions / mix test (OTP 24.3 | Elixir 1.13.4)

variable "component" does not exist and is being expanded to "component()", please use parentheses to remove the ambiguity or change the variable name

Check failure on line 734 in lib/phoenix_live_view/diff.ex

View workflow job for this annotation

GitHub Actions / mix test (OTP 24.3 | Elixir 1.13.4)

** (CompileError) lib/phoenix_live_view/diff.ex:734: undefined function component/0 (expected Phoenix.LiveView.Diff to define such a function or for it to be imported, but none are available)
[new_assigns] = module.preload([assigns])
new_assigns
else
Expand Down

0 comments on commit 58df0a6

Please sign in to comment.