diff --git a/lib/phoenix_live_component.ex b/lib/phoenix_live_component.ex
index ed344a1405..664b45df0a 100644
--- a/lib/phoenix_live_component.ex
+++ b/lib/phoenix_live_component.ex
@@ -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} />
@@ -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
@@ -121,23 +121,15 @@ defmodule Phoenix.LiveComponent do
Dismiss
- ### 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
@@ -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
@@ -189,36 +181,38 @@ defmodule Phoenix.LiveComponent do
```mermaid
flowchart LR
- *((start)):::event-.->P
- WE([wait for
parent changes]):::event-.->P
- W([wait for
events]):::event-.->H
+ *((start)):::event-.->M
+ WE([wait for
parent changes]):::event-.->M
+ W([wait for
events]):::event-.->H
- subgraph j__transparent[" "]
+ subgraph j__transparent[" "]
- subgraph i[" "]
- direction TB
- P(preload/1):::callback-->M(mount/1)
- M(mount/1
only once):::callback-->U
- end
+ subgraph i[" "]
+ direction TB
+ M(mount/1
only once):::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
changes?}:::diamond
- end
+ subgraph j[" "]
+ direction TB
+ A --> |yes| R
+ H(handle_event/3):::callback-->A{any
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
"""
@@ -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()
@@ -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
diff --git a/lib/phoenix_live_view/diff.ex b/lib/phoenix_live_view/diff.ex
index bda23623f6..8f606a1c5d 100644
--- a/lib/phoenix_live_view/diff.ex
+++ b/lib/phoenix_live_view/diff.ex
@@ -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}
@@ -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} =
@@ -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)
@@ -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")
[new_assigns] = module.preload([assigns])
new_assigns
else