Reducing Client-Side State with Phoenix LiveView's Server-Side Model
Stop syncing state between JS frameworks and APIs. Learn how Phoenix LiveView uses server-side processes and binary diffs to handle real-time UI state efficiently.
05 Feb 2026, 19:13 UTC

The State Synchronization Struggle
Building real-time interfaces usually requires maintaining two versions of the same state: one in your database/server and one in a JavaScript framework on the client. Keeping these in sync involves writing REST or GraphQL endpoints, managing complex frontend state stores (like Redux or Vuex), and handling the inevitable race conditions when the client and server disagree.
The core takeaway is that Phoenix LiveView eliminates this duality by moving the state management entirely to the server, using a persistent process to track the UI state and pushing only the necessary changes to the browser.
The Stateful Process Model
In a traditional web app, the server is stateless; it forgets who you are the moment the HTML is sent. LiveView changes this by assigning a dedicated Elixir process (a GenServer) to every connected user. This process stays alive for the duration of the session.
The UI state is stored in a map called assigns. When you update a value in assigns, LiveView automatically calculates the difference between the current HTML and the new state. Instead of reloading the page or sending a massive JSON blob for the frontend to parse, it sends a small binary diff over a WebSocket. The client-side LiveView JavaScript library then patches the DOM in place.
Handling Events Without API Endpoints
Because the state lives on the server, you no longer need to build a separate API layer for UI interactions. When a user clicks a button or types in a field, the event is routed directly to a handle_event/3 callback in your LiveView module.
This simplifies the development loop: you modify the server-side state, and the UI updates automatically. There is no "fetching" data after an action; the update is a side effect of the state change.
Example: A Real-Time Counter
To implement a simple interactive counter, you define the initial state in mount/3 and handle the increment logic in handle_event/3. This code runs entirely on the server.
def mount(_params, _session, socket) do
# Initialize the state in the assigns map
{:ok, assign(socket, :count, 0)}
end
def handle_event("increment", _params, socket) do
# Update the state; LiveView automatically pushes the diff to the client
{:noreply, update(socket, :count, &(&1 + 1))}
endIn the template, you simply bind the event to a button:
<button phx-click="increment">Count is: <%= @count %></button>Trade-offs and Constraints
Moving state to the server is a powerful architectural shift, but it introduces specific limitations:
- Memory Overhead: Since every user has a dedicated process on the server, memory usage scales linearly with the number of connected users. While Elixir processes are lightweight, they are not free.
- Latency Sensitivity: Because every interaction (like a button click) must travel to the server and back, users on high-latency connections may perceive a slight delay. For instant feedback (like a dropdown opening), you should use
phx-hookorJScommands to execute small bits of JavaScript locally. - Connectivity Dependency: LiveView is not an offline-first solution. If the WebSocket connection drops, the interface becomes non-interactive until the connection is restored.
Verifying the Efficiency
You can verify that LiveView is not sending full pages by using the Browser Developer Tools:
- Open the Network tab and filter by WS (WebSockets).
- Perform an action in your LiveView app (e.g., click the increment button).
- Observe the binary frames being sent. You will see small, encoded messages rather than full HTML documents or large JSON payloads.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.