LiveComponents are stateful components that run inside a LiveView process but maintain their own state and lifecycle. They provide a way to compartmentalize state, markup, and events while sharing the LiveView’s process.
When to Use LiveComponents
You need component-level state management
The component handles its own events
You want to encapsulate complex UI with behavior
You need lifecycle callbacks (mount, update)
Multiple instances need independent state
You only need to render UI (no state)
The component doesn’t handle events
You’re organizing markup for reuse
You want maximum simplicity and performance
General Rule : Prefer function components over LiveComponents. Only use LiveComponents when you specifically need stateful behavior with event handling. Don’t use them just for code organization.
Basic LiveComponent
Definition
defmodule MyAppWeb . HeroComponent do
use Phoenix . LiveComponent
def render (assigns) do
~H"""
< div class = "hero" > {@content} </ div >
"""
end
end
Rendering
<.live_component module={MyAppWeb.HeroComponent} id="hero" content={@content} />
The id attribute is required and must uniquely identify the component instance. The module attribute specifies which LiveComponent to render.
LiveComponent Lifecycle
LiveComponents have a lifecycle similar to LiveViews:
First Render: mount/1 -> update/2 -> render/1
Subsequent: -> update/2 -> render/1
Lifecycle Diagram
Lifecycle Callbacks
mount/1 - Component Initialization
Called once when the component is first added to the page.
@callback mount (socket :: Socket . t ()) ::
{ :ok , Socket . t ()} | { :ok , Socket . t (), keyword ()}
def mount (socket) do
{ :ok , assign (socket, :count , 0 )}
end
Unlike LiveView’s mount/3, LiveComponent’s mount/1 only receives the socket. It does not receive params or session.
update/2 - Receive New Assigns
Called whenever the parent LiveView re-renders or when the component receives new assigns.
@callback update (assigns :: Socket . assigns (), socket :: Socket . t ()) ::
{ :ok , Socket . t ()}
def update (assigns, socket) do
{ :ok ,
socket
|> assign (assigns)
|> load_user ()}
end
defp load_user (%{ assigns: %{ user_id: user_id}} = socket) do
assign (socket, :user , Accounts . get_user! (user_id))
end
After mount/1 on first render
Whenever the parent LiveView re-renders
When the component receives new assigns via send_update/3
Before each render/1 call
If you don’t define update/2, all assigns from live_component/1 are automatically merged into the socket.
update_many/1 - Batch Updates
Optional callback that receives all components of the same module being updated, enabling efficient batch operations.
@callback update_many ([{ Socket . assigns (), Socket . t ()}]) :: [ Socket . t ()]
Solving the N+1 Problem
def update (assigns, socket) do
user = Repo . get! ( User , assigns.id)
{ :ok , assign (socket, :user , user)}
end
If you render 100 user components, this executes 100 database queries! def update_many (assigns_sockets) do
list_of_ids =
Enum . map (assigns_sockets, fn {assigns, _socket } ->
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 (assigns_sockets, fn {assigns, socket} ->
assign (socket, :user , users[assigns.id])
end )
end
This executes just one query for all 100 components!
If update_many/1 is defined, update/2 is not invoked.
render/1 - Display the Component
@callback render (assigns :: Socket . assigns ()) ::
Phoenix . LiveView . Rendered . t ()
def render (assigns) do
~H"""
< div id = { "user-#{@id}"} class = "user" >
< h3 > {@user.name} </ h3 >
< p > {@user.email} </ p >
</ div >
"""
end
handle_event/3 - Component Events
@callback handle_event (
event :: binary,
unsigned_params :: Phoenix . LiveView . unsigned_params (),
socket :: Socket . t ()
) :: { :noreply , Socket . t ()} | { :reply , map, Socket . t ()}
def handle_event ( "increment" , _params , socket) do
{ :noreply , update (socket, :count , & ( &1 + 1 ))}
end
For events to reach a LiveComponent, the element must have a phx-target attribute.
Event Targeting
Targeting the Component Itself
Use @myself to send events to the current component:
<button phx-click="increment" phx-target={@myself}>
Count: {@count}
</button>
Targeting by ID
<!-- Target component with DOM ID "user-13" -->
<button phx-click="refresh" phx-target="#user-13">
Refresh User
</button>
Targeting by Class
<!-- Target all components with class "user-card" -->
<button phx-click="close" phx-target=".user-card">
Close All
</button>
Targeting Multiple Components
<button phx-click="close" phx-target="#modal, #sidebar">
Dismiss
</button>
State Management Patterns
LiveView as Source of Truth
The parent LiveView owns the data and passes it to components.
<!-- Parent LiveView -->
<.live_component
:for={card <- @cards}
module={CardComponent}
card={card}
id={card.id}
/>
# CardComponent
def handle_event ( "update_title" , %{ "title" => title}, socket) do
# Don't update local state - notify parent instead
send ( self (), { :updated_card , %{socket.assigns.card | title: title}})
{ :noreply , socket}
end
# Parent LiveView
def handle_info ({ :updated_card , card}, socket) do
cards = Enum . map (socket.assigns.cards, fn c ->
if c.id == card.id, do: card, else: c
end )
{ :noreply , assign (socket, :cards , cards)}
end
LiveComponent as Source of Truth
The component manages its own data independently.
<!-- Parent only passes ID -->
<.live_component
:for={card_id <- @card_ids}
module={CardComponent}
id={card_id}
/>
# CardComponent loads and manages its own data
def update_many (assigns_sockets) do
ids = Enum . map (assigns_sockets, fn {assigns, _ } -> assigns.id end )
cards =
from (c in Card , where: c.id in ^ids , select: {c.id, c})
|> Repo . all ()
|> Map . new ()
Enum . map (assigns_sockets, fn {assigns, socket} ->
assign (socket, :card , cards[assigns.id])
end )
end
def handle_event ( "update_title" , %{ "title" => title}, socket) do
card = %{socket.assigns.card | title: title}
Cards . update_card (card)
{ :noreply , assign (socket, :card , card)}
end
Components don’t have handle_info/2. To receive PubSub messages, the parent LiveView must receive them and forward via send_update/3.
Communicating Between Components
Using Callbacks (Recommended)
Pass callback functions as assigns:
<.live_component
module={CardComponent}
id={@card.id}
card={@card}
on_update={fn card -> send(self(), {:updated_card, card}) end}
/>
def handle_event ( "save" , params, socket) do
socket.assigns. on_update .(updated_card)
{ :noreply , socket}
end
Using send_update/3
Update a component programmatically from anywhere:
# From LiveView
def handle_info ({ :refresh_card , card_id}, socket) do
send_update ( CardComponent , id: card_id, refresh: true )
{ :noreply , socket}
end
# From another component
def handle_event ( "refresh_all" , _params , socket) do
send_update ( CardComponent , id: "card-1" , refresh: true )
{ :noreply , socket}
end
Sends a message to update a specific component
Triggers the component’s update/2 or update_many/1 callback
Can target components by module + id or by @myself
Useful for updating components from outside their render tree
Slots in LiveComponents
LiveComponents support slots just like function components:
slot :inner_block , required: true
def render (assigns) do
~H"""
< div class = "wrapper" >
{render_slot(@inner_block)}
</ div >
"""
end
Usage:
<.live_component module={MyComponent} id="wrapper">
<p>Inner content here</p>
</.live_component>
If you define update/2, ensure it preserves the :inner_block assign: def update (assigns, socket) do
{ :ok , assign (socket, assigns)}
end
Async Operations
LiveComponents can use handle_async/3 for async work:
def update (assigns, socket) do
socket =
socket
|> assign (assigns)
|> assign_async ( :data , fn ->
{ :ok , %{ data: fetch_data (assigns.id)}}
end )
{ :ok , socket}
end
def handle_async ( :data , { :ok , %{ data: data}}, socket) do
{ :noreply , assign (socket, :data , data)}
end
def handle_async ( :data , { :exit , reason}, socket) do
{ :noreply , put_flash (socket, :error , "Failed to load data" )}
end
Live Navigation
Components can use push_patch/2 and push_navigate/2:
<.link patch={~p"/items/#{@item.id}"}>View Item</.link>
Live patches are always handled by the parent LiveView’s handle_params/3, not the component.
Memory Usage
LiveComponents keep all assigns in memory (just like LiveViews). Be mindful:
<!-- BAD - passes all parent assigns -->
<.live_component module={MyComponent} {assigns} />
<!-- GOOD - only pass what's needed -->
<.live_component module={MyComponent} user={@user} id={@user.id} />
Change Tracking
LiveView tracks changes efficiently, but only at the component boundary:
# If only @user.name changes, only that component re-renders
< .live_component module = { UserCard } id = {user.id} name = {user.name} />
Optimization Tips
Use update_many/1 for batch operations
Prevents N+1 queries when rendering multiple instances.
Only store what the component actually needs.
Prefer function components when possible
They’re simpler and have less overhead.
Common Patterns
Modal Component
defmodule MyAppWeb . ModalComponent do
use Phoenix . LiveComponent
attr :title , :string , required: true
attr :on_close , :any , required: true
slot :inner_block , required: true
def render (assigns) do
~H"""
< div class = "modal" phx-click-away = {@on_close} >
< div class = "modal-content" >
< div class = "modal-header" >
< h2 > {@title} </ h2 >
< button phx-click = "close" phx-target = {@myself} > × </ button >
</ div >
< div class = "modal-body" >
{render_slot(@inner_block)}
</ div >
</ div >
</ div >
"""
end
def handle_event ( "close" , _params , socket) do
socket.assigns. on_close .()
{ :noreply , socket}
end
end
defmodule MyAppWeb . UserFormComponent do
use Phoenix . LiveComponent
def mount (socket) do
{ :ok , socket}
end
def update (assigns, socket) do
changeset = Accounts . change_user (assigns.user)
{ :ok , assign (socket, assigns) |> assign ( :form , to_form (changeset))}
end
def render (assigns) do
~H"""
<.form for={@form} phx-submit="save" phx-target={@myself}>
<.input field={@form[:name]} label="Name" />
<.input field={@form[:email]} label="Email" />
<.button>Save</.button>
</.form>
"""
end
def handle_event ( "save" , %{ "user" => user_params}, socket) do
case Accounts . update_user (socket.assigns.user, user_params) do
{ :ok , user} ->
send ( self (), { :user_updated , user})
{ :noreply , socket}
{ :error , changeset} ->
{ :noreply , assign (socket, :form , to_form (changeset))}
end
end
end
defmodule MyAppWeb . InfiniteScrollComponent do
use Phoenix . LiveComponent
def update (assigns, socket) do
{ :ok ,
socket
|> assign (assigns)
|> assign_new ( :page , fn -> 1 end )
|> load_items ()}
end
def render (assigns) do
~H"""
< div id = "items" phx-hook = "InfiniteScroll" phx-target = {@myself} >
< div :for = {item <- @items} class = "item" >
{item.title}
</ div >
< div phx-click = "load-more" phx-target = {@myself} > Load More </ div >
</ div >
"""
end
def handle_event ( "load-more" , _params , socket) do
{ :noreply , socket |> update ( :page , & ( &1 + 1 )) |> load_items ()}
end
defp load_items (socket) do
items = Content . list_items ( page: socket.assigns.page)
update (socket, :items , & ( &1 ++ items))
end
end
Testing LiveComponents
import Phoenix . LiveViewTest
test "renders component" do
html = render_component ( & MyComponent . render / 1 , id: "test" , user: % User {})
assert html =~ "Hello"
end
test "handles events" do
{ :ok , view, _html } = live (conn, "/page" )
html =
view
|> element ( "#my-component button" )
|> render_click ()
assert html =~ "Updated"
end
Summary
LiveComponents maintain their own state within the parent LiveView’s process
Always pass unique id and module attributes
Lifecycle: mount/1 → update/2 → render/1 → handle_event/3
Use update_many/1 to prevent N+1 queries
Events require phx-target={@myself} to reach the component
Use send_update/3 to update components programmatically
Prefer function components unless you need stateful behavior
Keep component assigns minimal for better performance
Use callbacks for parent-child communication