All LiveView data is stored in the socket under the assigns key. The way you work with assigns directly impacts performance, as LiveView uses them for change tracking to minimize data sent over the wire.
Understanding Assigns
Assigns are a map stored in socket.assigns that hold all the data your LiveView needs:
# In your LiveView code
socket.assigns.user
socket.assigns.count
socket.assigns.posts
# In your templates
@user
@count
@posts
The @ syntax in templates is syntactic sugar for assigns.key. Behind the scenes, @user becomes assigns[:user].
Setting Assigns
assign/2 - Set Multiple Assigns
# With a keyword list
assign (socket, name: "Alice" , age: 30 )
# With a map
assign (socket, %{ name: "Alice" , age: 30 })
# With a function
assign (socket, fn assigns ->
%{ full_name: " #{ assigns.first_name } #{ assigns.last_name } " }
end )
assign/3 - Set a Single Assign
assign (socket, :count , 0 )
assign (socket, :user , user)
assign (socket, :loading , false )
Chaining Assigns
All assign functions return an updated socket, enabling chaining:
socket
|> assign ( :loading , true )
|> assign ( :count , 0 )
|> assign ( :posts , [])
Updating Assigns
# Increment a counter
update (socket, :count , fn count -> count + 1 end )
update (socket, :count , & ( &1 + 1 ))
# Append to a list
update (socket, :posts , fn posts -> [new_post | posts] end )
# Update a map
update (socket, :user , & Map . put ( &1 , :verified , true ))
update/3 raises a KeyError if the key doesn’t exist. Use assign/3 if the key might not be present.
update/3 with Two-Arity Function
Access other assigns while updating:
update (socket, :total , fn _current , assigns ->
Enum . sum (assigns.values)
end )
Lazy Assignment
assign_new/3 - Only Set If Not Present
def mount ( _params , _session , socket) do
socket =
socket
|> assign_new ( :count , fn -> 0 end )
|> assign_new ( :current_user , fn -> load_user () end )
{ :ok , socket}
end
When is assign_new useful?
Setting default values
Sharing assigns from plugs during disconnected render
Sharing assigns between parent and child LiveViews
Lazy loading expensive data only when needed
Sharing from Plug Pipeline
# In a plug
def assign_user (conn, _opts ) do
assign (conn, :current_user , get_user (conn))
end
# In LiveView mount
def mount ( _params , %{ "user_id" => user_id}, socket) do
socket = assign_new (socket, :current_user , fn ->
# Only runs if not already set by plug
Accounts . get_user! (user_id)
end )
{ :ok , socket}
end
Change Tracking
LiveView tracks which assigns have changed and only re-renders affected parts of the template.
How It Works
# Initial render
assign (socket, name: "Alice" , age: 30 , city: "NYC" )
# Later, update only name
assign (socket, :name , "Bob" )
# LiveView knows:
# - @name changed → re-render parts using @name
# - @age unchanged → don't re-render parts using @age
# - @city unchanged → don't re-render parts using @city
Nested Field Tracking
Change tracking works with map/struct fields:
<div id={"user-#{@user.id}"}>
{@user.name}
</div>
If you update:
update (socket, :user , & Map . put ( &1 , :name , "New Name" ))
LiveView knows:
@user.name changed → re-render
@user.id unchanged → don’t re-render or resend
Common Pitfalls
Variables Disable Tracking
Don’t do this - variables disable change tracking:<% total = @x + @y %>
<p>{total}</p>
LiveView must re-render this on every render, even if @x and @y haven’t changed.
Do this instead - use assigns:def render (assigns) do
assigns = assign (assigns, :total , assigns.x + assigns.y)
~H"<p>{@total}</p>"
end
Now LiveView only re-renders when @x or @y actually change.
Don’t Access assigns Directly
Don’t do this: <% some_value = assigns.x + assigns.y %>
{some_value}
Direct access to the assigns variable disables change tracking.
Do this instead: <.component x={@x} y={@y} />
Never Use Map Functions on Assigns
Never modify assigns with Map functions: def my_component (assigns) do
assigns = Map . put (assigns, :computed , compute (assigns.value))
~H"<p>{@computed}</p>"
end
This breaks change tracking completely!
Always use LiveView’s assign functions: def my_component (assigns) do
assigns = assign (assigns, :computed , compute (assigns.value))
~H"<p>{@computed}</p>"
end
No Data Loading in Templates
Never load data in templates: <%= for post <- Blog.list_posts() do %>
{post.title}
<% end %>
This query runs on every render , and LiveView won’t detect changes!
Load data in mount or handle_ callbacks: *def mount ( _params , _session , socket) do
{ :ok , assign (socket, :posts , Blog . list_posts ())}
end
Reserved Assigns
Some assign keys are reserved by LiveView:
@flash - Flash messages (use put_flash/3)
@uploads - File uploads (use allow_upload/3)
@streams - Streamed collections (use stream/4)
@socket - The socket struct itself
@myself - Component reference (LiveComponent only)
@live_action - Current live action from router
Attempting to assign these keys directly will raise an error.
Temporary Assigns
Temporary assigns are reset to their initial value after every render:
def mount ( _params , _session , socket) do
{ :ok , socket, temporary_assigns: [ posts: []]}
end
def handle_event ( "load_posts" , _params , socket) do
# Assign new posts
socket = assign (socket, :posts , Blog . list_posts ())
{ :noreply , socket}
# After render, @posts automatically resets to []
end
When to use temporary assigns?
Sending large collections that you don’t want to keep in memory
One-time data that’s only needed for a single render
Flash-style messages that should disappear
Large file upload metadata
Note: For most collection use cases, prefer stream/4 instead.
Streams
Streams allow you to manage collections without keeping them in memory:
def mount ( _params , _session , socket) do
{ :ok , stream (socket, :posts , Blog . list_posts ())}
end
def handle_event ( "add_post" , %{ "post" => post_params}, socket) do
post = Blog . create_post! (post_params)
{ :noreply , stream_insert (socket, :posts , post, at: 0 )}
end
def handle_event ( "delete_post" , %{ "id" => id}, socket) do
Blog . delete_post! (id)
{ :noreply , stream_delete (socket, :posts , %{ id: id})}
end
Streams are ideal for large lists, infinite scroll, and real-time feeds where you don’t need to keep all items in memory.
State Patterns
Loading State Pattern
def mount ( _params , _session , socket) do
socket =
socket
|> assign ( :loading , true )
|> assign ( :data , nil )
|> assign ( :error , nil )
if connected? (socket) do
send ( self (), :load_data )
end
{ :ok , socket}
end
def handle_info ( :load_data , socket) do
case fetch_data () do
{ :ok , data} ->
{ :noreply , assign (socket, loading: false , data: data)}
{ :error , error} ->
{ :noreply , assign (socket, loading: false , error: error)}
end
end
In template:
<div :if={@loading}>Loading...</div>
<div :if={@error}>Error: {@error}</div>
<div :if={@data}>
<!-- Render data -->
</div>
def mount ( _params , _session , socket) do
changeset = Accounts . change_user (% User {})
{ :ok , assign (socket, form: to_form (changeset))}
end
def handle_event ( "validate" , %{ "user" => user_params}, socket) do
changeset =
% User {}
|> Accounts . change_user (user_params)
|> Map . put ( :action , :validate )
{ :noreply , assign (socket, form: to_form (changeset))}
end
def handle_event ( "save" , %{ "user" => user_params}, socket) do
case Accounts . create_user (user_params) do
{ :ok , user} ->
{ :noreply , push_navigate (socket, to: ~p"/users/ #{ user } " )}
{ :error , changeset} ->
{ :noreply , assign (socket, form: to_form (changeset))}
end
end
def mount ( _params , _session , socket) do
socket =
socket
|> assign ( :page , 1 )
|> assign ( :per_page , 20 )
|> load_page ()
{ :ok , socket}
end
def handle_event ( "next_page" , _params , socket) do
socket =
socket
|> update ( :page , & ( &1 + 1 ))
|> load_page ()
{ :noreply , socket}
end
defp load_page (socket) do
%{ page: page, per_page: per_page} = socket.assigns
posts = Blog . list_posts ( page: page, per_page: per_page)
assign (socket, :posts , posts)
end
Tab State Pattern
def handle_params (params, _uri , socket) do
tab = params[ "tab" ] || "overview"
{ :noreply , assign (socket, :active_tab , tab)}
end
def handle_event ( "switch_tab" , %{ "tab" => tab}, socket) do
{ :noreply , push_patch (socket, to: ~p"/dashboard?tab= #{ tab } " )}
end
Search State Pattern
def mount ( _params , _session , socket) do
socket =
socket
|> assign ( :search_query , "" )
|> assign ( :search_results , [])
{ :ok , socket}
end
def handle_event ( "search" , %{ "query" => query}, socket) do
results = Search . search (query)
socket =
socket
|> assign ( :search_query , query)
|> assign ( :search_results , results)
{ :noreply , socket}
end
Async Assigns
For expensive operations, use assign_async/3:
def mount (%{ "slug" => slug}, _session , socket) do
{ :ok ,
socket
|> assign ( :slug , slug)
|> assign_async ( :org , fn ->
{ :ok , %{ org: Orgs . fetch_by_slug! (slug)}}
end )}
end
In template:
<div :if={@org.loading}>Loading organization...</div>
<div :if={@org.ok? && @org.result}>
{@org.result.name}
</div>
Don’t pass the socket to async functions: # BAD - copies entire socket
assign_async ( :org , fn -> fetch_org (socket.assigns.slug) end )
# GOOD - only copy what you need
slug = socket.assigns.slug
assign_async ( :org , fn -> fetch_org (slug) end )
Debugging Assigns
Inspect in Templates
<pre>{inspect(@socket.assigns, pretty: true)}</pre>
<pre>{inspect(@user, pretty: true)}</pre>
Inspect in Code
def handle_event ( "debug" , _params , socket) do
IO . inspect (socket.assigns, label: "Current Assigns" )
{ :noreply , socket}
end
Using IEx
def handle_event ( "breakpoint" , _params , socket) do
require IEx ; IEx . pry ()
{ :noreply , socket}
end
Best Practices
Only store what you actually need. Large data structures consume memory and slow down change tracking.
# Good
assign (socket, :user_search_results , results)
# Bad
assign (socket, :results , results)
Initialize all assigns in mount
Set default values for all assigns your LiveView uses: def mount ( _params , _session , socket) do
socket =
socket
|> assign ( :loading , false )
|> assign ( :error , nil )
|> assign ( :data , [])
{ :ok , socket}
end
Use update for transformations
Summary
Assigns are stored in socket.assigns and accessed as @key in templates
Use assign/2, assign/3, and assign_new/3 to set assigns
Use update/3 to transform existing assigns
Change tracking automatically optimizes what’s sent to the client
Never access the assigns variable directly in templates
Never use Map functions to modify assigns
Avoid variables in templates (they disable tracking)
Load data in callbacks, not in templates
Use temporary assigns or streams for large collections
Keep assigns minimal for better performance