Why Our Analytics Dashboard Is Phoenix LiveView, Not React
When you build an analytics dashboard — accounts, PnL over a date range, profit/loss breakdowns, live mark prices — the reflexive choice is a React single-page app talking to a JSON API. For our trading platform's analytics, I went with Phoenix LiveView instead. This is why, and where I think LiveView is genuinely the wrong tool.
The default I didn't pick
A React dashboard isn't one thing — it's a stack. You write a JSON API and serializers on the server, a typed client to call it, a cache layer (React Query or Redux) to hold the results, and — because this is a real-time product — a separate WebSocket channel plus the reconciliation logic to fold live updates into that cache. Now the same piece of state (a position's PnL) exists in two places: the server that computed it and the browser that's caching it. Most dashboard bugs live in the gap between those two copies.
Advantage 1: real-time is the default, not an add-on
The dashboard lives in the same Elixir umbrella as the trading backbone, where account and market data already flow over a clustered Phoenix.PubSub topic. A LiveView just subscribes to that topic in mount and pushes a rendered diff when data changes — live PnL with no extra moving parts. In the React version, "make it live" is a whole sub-project: a socket layer, an event protocol, and client cache invalidation. In LiveView it's three lines and a handle_info.
Advantage 2: one language, one source of truth
The server holds the UI state. There's no duplicated model on the client, no API contract to keep in sync between a TypeScript frontend and an Elixir backend, no serialization layer to maintain. When the shape of a PnL breakdown changes, I change it in one place. The whole category of "the frontend and backend disagree about this field" bugs simply doesn't exist.
Advantage 3: compute where the data is
Analytics means aggregation — summing PnL across a date range, grouping by account, ranking symbols. That work belongs next to the database, not in a browser. With LiveView I query and aggregate on the server and send the browser a rendered table diff — not a megabyte of JSON for the client to re-crunch on every filter change. For data-heavy views over large tables, that's the difference between snappy and sluggish, especially on a phone.
Advantage 4: dramatically less code
The interactive pieces — a date-range picker, tabs, account selector, paginated tables — are LiveComponents: stateful, reusable UI units written in Elixir with the markup right next to the logic. No fetch boilerplate, no loading/error/success state machine for every request, no build-it-twice. A feature that's a backend endpoint plus a frontend screen in the React world is one LiveView here.
Advantage 5: it's a peer in the umbrella
Because the analytics app is just another app in the umbrella, it shares auth, the database layer, PubSub, and telemetry with everything else. It reads the same account data the rest of the platform produces, authenticates users the same way, and reports metrics to the same Prometheus. A separate React frontend would have been a separate deployable with its own auth integration, its own build pipeline, and its own ops surface.
Advantage 6: sensitive data stays server-side
On a financial product, the less account data that reaches the browser, the better. LiveView sends rendered output, not raw records and not the queries behind them. Business logic and data never leave the server; the client gets exactly what's on screen and nothing more.
Where LiveView is the wrong call
I'd be selling you something if I pretended it's always right. LiveView assumes a live connection to the server, so it's a poor fit for offline-first apps, native mobile, or interactions that must feel instant on a flaky network — every event is a round trip. And some UI is genuinely client-side: a candlestick chart you pan and zoom at 60 fps doesn't want a server round trip per frame.
The honest answer is that it's not all-or-nothing. For the few truly client-heavy widgets — interactive charts, for instance — LiveView has JS hooks: you drop to a small piece of JavaScript exactly where it earns its keep, and let LiveView own everything else. You write JS where it pays, not by default.
What bit us in production
Honesty time, because this is the part people skip. Early on we had a memory scare: with only two or three users online — each owning around a hundred trading accounts — pods would lag and then run out of memory. The instinct was to blame LiveView: "everything renders on the server, so of course it falls over." That was the wrong diagnosis, and chasing it would have wasted a week.
The render path was cheap — each viewer was looking at one account's small PnL summary. The real culprit was the real-time data layer, and it had nothing to do with rendering. We were eagerly starting a live pipeline per account that existed, not per account anyone was watching: an upstream socket, a couple of GenServers, ETS tables, and a one-second push loop, for every account in the system. "Three users × a hundred accounts" meant a few hundred live pipelines, hundreds of ETS tables, and hundreds of broadcasts per second — whether or not a human was looking at any of it.
Two BEAM details turned that from "wasteful" into "OOM":
- ETS isn't on a process heap. Those per-account tables sit in node memory until you delete them — they don't get garbage-collected away when nobody's using them.
- Messages between processes are copied. Broadcasting a full position payload to every subscriber once a second copies it once per subscriber, and any consumer that falls behind grows an unbounded mailbox — the classic path to running out of memory.
The fix wasn't to abandon LiveView — it was to make the data layer lazy and viewer-scoped: start an account's pipeline only when someone actually views it, tear it down when the last viewer leaves (tracked with Presence), and compute on render or send deltas instead of pushing the full state every second. That collapsed "a few hundred pipelines because they exist" into "a handful because someone's watching." The irony is that LiveView made the fix easy — Presence and PubSub were already there. The lesson had nothing to do with where rendering happens: doing work for data nobody is looking at is what kills you, in any stack.
What I take away
Choosing LiveView wasn't about avoiding JavaScript — it was about deleting an entire tier. No API contract, no client-side cache to reconcile, no second codebase, and real-time for nearly free because the data was already moving through Elixir. For a data-heavy, real-time dashboard backed by an Elixir system, the server-rendered path is less code doing more — as long as you only do that work for data someone is actually watching. React is a fine tool; here it would have been a tier of machinery to solve a problem I didn't need to have.