We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Stop mounting your LiveView twice
almirsarajcic
Every LiveView mounts twice on first page load — once for the static HTTP “dead render,” then again when the WebSocket connects. If mount/3 loads data unconditionally, you run that query, hit that cache, and rebuild those assigns two times for a single visit. Guard the work you don’t need for first paint with connected?/1:
# ❌ Runs your query TWICE on every first page load
def mount(_params, _session, socket) do
{:ok, assign(socket, :posts, Blog.list_posts())}
end
# ✅ Skip the dead-render query; load once, when connected
def mount(_params, _session, socket) do
posts = if connected?(socket), do: Blog.list_posts(), else: []
{:ok, assign(socket, :posts, posts)}
end
That’s the conventional fix — the one you’ll find in every LiveView thread. connected?(socket) is false during the dead render and true once the socket is live, so the guarded branch runs exactly once. Subscriptions, timers, presence tracking, and expensive loads all get parked behind it.
Here’s the catch, though: that guard is a band-aid, not a cure. It doesn’t even stop the double render — mount/3, render/1, and your on_mount hooks all still run twice; it just trades your query for an empty first paint — quietly taking your SEO, your link previews, and your no-JavaScript fallback down with it. By the end of this post you’ll have a sharper default — load your data plainly, move connection-only work into an on_connect/1 callback, and let the dead render’s work be reused on connect instead of repeated. But to get there you first have to understand why LiveView renders twice at all, what connected?/1 actually costs you, and what the Phoenix team is building to erase the double mount for good.
Why a LiveView renders twice
When a browser first requests a LiveView route, there is no WebSocket yet — just a plain HTTP request. Phoenix can’t stream diffs over a socket that doesn’t exist, so it does the pragmatic thing: it runs your LiveView once as an ordinary request and returns static HTML. This is the dead render (a.k.a. the disconnected render). Then the JavaScript client boots, opens a WebSocket, and Phoenix runs your LiveView again — this time as a long-lived stateful process that pushes diffs. This is the connected render.
# FIRST PAGE LOAD = TWO mounts, not one
#
# 1. HTTP request -> DEAD render (static HTML: fast first paint, SEO)
# mount/3 connected?(socket) == false
# handle_params/3
# render/1 -> full HTML sent over the wire
#
# 2. WebSocket join -> CONNECTED render (the stateful process)
# mount/3 connected?(socket) == true <- runs AGAIN
# handle_params/3
# render/1 -> FULL render sent (not a diff), page becomes "live"
So mount/3, handle_params/3, and render/1 all run twice on first load. The dead render earns its keep for two real reasons:
- First paint and SEO. The user (and Googlebot) get meaningful HTML immediately, before any JavaScript executes.
- Progressive enhancement. Even if the WebSocket never connects, the page still shows something useful.
The connected render exists because that is where LiveView actually lives: a process that holds state, receives messages, and pushes minimal diffs. Two different jobs, two passes through your mount/3.
What connected?/1 actually costs you
Here’s what the one-liner tip never says out loud: the guard doesn’t remove the double render — it just makes the dead render useless for the one job it exists to do. mount/3, render/1, and every on_mount hook still run twice; you’ve only swapped your expensive query for an empty branch. The structural duplication — auth, current_user, layout, the per-request work in your hooks, which is often where the real cost lives — is completely untouched. And in exchange for cheapening that one branch, you take on four real costs:
- An empty first paint. No data on the dead render means a skeleton, a spinner, or a blank box until the WebSocket finishes its round-trip. That pushes Largest Contentful Paint behind the socket, and the content popping in afterward shifts the layout unless your placeholder reserves the final content’s exact space — two Core Web Vitals that Google scores you on.
- Broken SEO and link previews. This is the big one, and it is not just “Googlebot.” Many crawlers don’t reliably run JavaScript — Bing’s rendering is limited and inconsistent, and every social unfurler (Slack, X, Facebook, LinkedIn, iMessage) that fetches your page to build an Open Graph card runs none at all. They get the dead render, and the dead render is empty. Even Googlebot, which does render JS, defers it to a later pass and makes no promise it will open and await your LiveView socket — WebSocket-delivered content is not server-rendered HTML. And your social tags make this concrete:
og:image,og:description, and<meta name="description">live in your root layout, which renders exactly once — on the dead render — and is never diffed again. They can only ever reflect what the dead render knew; load that data behindconnected?/1and the tags stay empty for everyone, with no second pass to fix them. (The page<title>is the one exception — LiveView’s<.live_title>can update it live — but a crawler that never runs your JS still indexes the empty dead-render title.) - A page that needs JavaScript to show anything. The dead render exists so the page works before — and even without — JS. Guard your content behind
connected?/1and a disabled script, a WebSocket-blocking corporate proxy, a flaky network, or a single failed bundle leaves the visitor staring at your skeleton forever. You’ve turned graceful degradation into a hard dependency. - Connection logic smeared through
mount/3. Everyif connected?(socket)is a little fork in your setup code — easy to misplace, easy to forget, and it only multiplies as the LiveView grows.
So every option stock LiveView gives you today is a compromise:
- Load unconditionally → great first paint, but double the DB/cache work on every cold visit.
- Guard with
connected?/1→ single load, but an empty first paint, broken SEO, and a JS-dependent page. - Load a cheap subset on the dead render and the full set on connect → more code, still two passes, and now you’re hand-maintaining “what’s cheap enough for first paint.”
There is no way, in stock LiveView right now, to load your data once and still have it present in the first paint. That is the real problem — and it’s exactly what’s being worked on.
Aside: to tell a genuine first connect from a reconnect, read
get_connect_params(socket)["_mounts"]. It’s0on the first mount and increments on each reconnect — handy for logic that should run only on the initial connection. (Subscriptions usually aren’t that case: they belong on every connection, reconnects included — which is exactly whyon_connect/1below fires on reconnect too.)
What the Phoenix team is building: adoptable LiveViews
In December 2024, José Valim opened issue #3551, “Allow LiveViews to be adopted.” It attacks the double render at the root: instead of throwing away the dead-render process and spinning up a fresh one on connect, keep the process alive and let the WebSocket adopt it.
One mechanism, two wins:
- On first load: the dead render spawns the LiveView process and keeps it alive; when the WebSocket arrives it adopts that exact process and receives only the latest diff — no second mount, no second render, zero copying.
- On disconnect: the process is kept alive for a few seconds, so a reconnecting client re-links to the same process and gets a diff instead of a full remount with lost state.
José notes this should be “strictly better than a cache layer for a single tab” — smaller payloads on both the connected render and on reconnects.
It is not trivial to design, which is why it’s still open. The hard parts:
- Clustering. If the reconnect lands on a different node than the one holding the process, you must choose between cluster round-trips on every payload, copying a subset of assigns (the
assign_newapproach), or discarding the orphan and remounting. - Security / DoS. Keeping processes alive for clients that may never connect is a memory-amplification vector, so it has to be opt-in with short timeouts — especially on public pages.
- Patch management. An orphaned LiveView must replay queued patches correctly on adoption rather than squashing them into one.
As of mid-2026, #3551 is still open: the direction is accepted, but there’s no merged implementation and nothing shipped on Hex yet.
The interim: a fork that parks the dead-render socket
A personal aside on how this fork came to be. I’ve watched mount/3 fire twice since the day I first picked up LiveView. My response was always the same: accept it. Let all the code run in both the dead and the connected render, put subscriptions behind connected?/1 because those genuinely have to be — and, on the rare occasion something was truly expensive, guard that one piece too. Then move on, never once digging into why it had to be that way or whether it could be avoided. The cost of actually finding out (reading the channel internals, reasoning through dead render vs. connect, then handling reconnects, clustering, and every edge case) was always higher than the itch to know. So for years it stayed a thing I’d accepted without understanding.
What changed isn’t LiveView — it’s that I can now hand a capable AI agent a rough sketch of an idea (“park the dead-render socket, redeem it on connect”) and have it go digging through the framework for me: prove the approach out, surface the lifecycle implications, and cover the cases I’d never have had the patience to chase by hand. This fork is the result of exactly that — me finally seeing the double mount for myself, by letting an agent do the diving.
ElixirDrops itself runs on a fork of phoenix_live_view that solves the narrower, most common case — the first-load double mount — while adoption is designed upstream. It adds an opt-in :resume feature. The full diff is on the fork if you want to read the source; what matters here is the shape of it:
- The dead render mounts as usual, fully. Instead of discarding the resulting socket, it hands it to a short-lived holder process — a
GenServerunder aDynamicSupervisor, registered by a random key in aRegistry. - It signs that key into a token — bound to the endpoint salt, the socket
id, and the view module — and emits it as adata-phx-resumeattribute on the root element. - On WebSocket connect, the client sends the token back. The server verifies it, looks up the holder, and redeems the parked socket in a single-use
GenServer.call. The holder then stops and unregisters itself. - The parked state is spliced into the live socket and the connected process reuses the whole mounted result — neither
mount/3nor theon_mounthooks run again.
That last part is the bigger win, and it’s easy to miss. The duplication was never just your mount/3 — it was the entire on_mount lifecycle: every on_mount hook your live_session declares (auth/current_user, layout, presence, notification badges, per-request data) runs on the dead render and again on connect. Those hooks are often where the real cost is. Because resume reuses the fully-mounted socket, all of that work happens exactly once. The flip side is that anything that genuinely must run per-connection (and not on the dead render) can’t ride along in a hook anymore — it moves to on_connect/1.
The public surface is small (the internals are deliberately private):
# config/config.exs — opt in for the whole app
config :phoenix_live_view, :resume,
enabled: true,
# how long a parked socket waits for its WebSocket, in ms
ttl: 5_000,
# backpressure: cap on concurrently parked sockets
max_children: 10_000
# ...or opt in per LiveView (overrides the app-wide default)
defmodule MyAppWeb.DashboardLive do
use Phoenix.LiveView, resume: true
# ...
end
The properties that make it safe to run in production:
- Single-use token. The holder stops the instant it’s redeemed, so a token cannot be replayed.
- Forgery-proof. The token is signed with the endpoint’s secret (via
Phoenix.Token) and carries the socketidand view module in its payload, so a tampered token fails verification and a token minted for one view can’t be replayed against another. - Fail-closed TTL. If the WebSocket never arrives, the holder times out (default 5s) and the registry lookup simply comes back empty.
- Backpressure.
max_childrencaps parked sockets; at capacity, issuing a token fails and the page silently falls back to a normal cold mount. - Cross-node safe. If the connect lands on a node that doesn’t hold the holder, the lookup misses and you get a cold mount — never a crash.
And the cost side of the ledger, stated plainly: resume isn’t free. It holds each dead-render socket in memory until its WebSocket connects or the TTL fires — so you’re trading a little RAM (bounded by max_children, with a short TTL) for the saved queries and hook work. And it’s a server-CPU/DB win, not a bandwidth one: the first connect still ships a full render (see the table below), so it doesn’t shrink the payload — it stops you computing that payload’s data twice. If your bottleneck is wire bytes rather than backend work, this isn’t the lever; that’s the upstream adoption design’s job.
What runs when
The whole model fits in one table. This is the mental model to internalize before you migrate anything:
| Callback | Dead render (HTTP) | Cold connect (no resume) | Warm connect (resumed) |
|---|---|---|---|
mount/3 |
✅ runs | ✅ runs again | ⛔ skipped |
on_mount hooks |
✅ run | ✅ run again | ⛔ skipped |
on_connect/1 |
— (not connected) | ✅ runs | ✅ runs |
handle_params/3 |
✅ runs | ✅ runs again | ⛔ skipped ¹ |
render/1 |
✅ (static HTML) | ✅ (full render) | ✅ (full render) |
¹ handle_params/3 is skipped only on the initial resumed connect — its dead-render result is already in the spliced socket, so re-running it with the identical URL would just redo the work. It still runs normally on later patch navigation, and after a live navigation mounts the destination LiveView.
The warm column is the whole point. On a resumed connect the server splices the parked dead-render socket back in and renders it — so every assign your mount/3, on_mount hooks, and handle_params/3 produced on the dead render is already there, no recomputation. The single callback that runs is on_connect/1, because that’s the one place genuinely per-connection work lives.
One thing the render/1 row does not say: that the connect sends a “diff.” It doesn’t — not on the first connect. The connect is a fresh channel join with empty fingerprints, so LiveView renders and ships the whole tree (statics + every dynamic), exactly like the dead render did, just in the socket wire format instead of HTML. (Diffs only start on subsequent updates, once the client has a tree to diff against.) This holds for cold and warm connects alike — resume’s win is that it skips the second mount and data load on the server, not that it shrinks the connect payload. That smaller-payload-on-connect property is what José’s adoption design (#3551) adds on top, by keeping the client’s tree continuous instead of re-joining.
But a full payload is not a full repaint, and this is the part that makes the whole approach sound. The client does not throw away the dead-render DOM and rebuild it from the connect reply — it runs the incoming tree through morphdom against the HTML already on the page, mutating only the nodes that genuinely differ. Once you load your data on the dead render, the dead render and the connect render are identical, so morphdom finds nothing to change: no nodes recreated, no flash, no layout shift. The full tree costs you bytes on the socket, not a visible re-render. (This is plain LiveView client behavior, not something resume adds — it’s also why the empty-first-paint flash from connected?/1 happens in the first place: there, morphdom does have work to do, because it’s reconciling an empty skeleton against a now-full tree. Give the dead render the real content and that reconciliation becomes a no-op.)
“Skipped” doesn’t mean dead — does handle_params/3 still fire when I navigate? Yes, and this is the question the table always raises, so it’s worth stating plainly. handle_params/3 is skipped on one event only: the initial resumed connect, where the dead render already ran it with the identical URL and re-running it would just redo the work. The instant the URL changes through a push_patch/2 or a <.link patch={...}>, handle_params/3 runs again, exactly as in stock LiveView. A live navigation (push_navigate/2, <.link navigate={...}>, JS.navigate/1) mounts the destination LiveView, after which its normal parameter handling runs. Pagination, filters, tabs, master/detail panes: all unchanged. The rule is simple — resume collapses the one redundant run (dead render + connect, same params) into a single run; it never suppresses a run where the params genuinely differ.
| Navigation | handle_params/3? |
|---|---|
| Initial resumed connect (URL identical to dead render) | ⛔ skipped — result already spliced in |
push_patch/2 / <.link patch={...}> (URL changes) |
✅ runs |
Live navigation to another LiveView (push_navigate/<.link navigate>/JS.navigate) |
✅ runs after the destination mounts |
And the scoping point that ties it together: resume only touches the first paint. A dead render happens on a fresh HTTP request — first visit, refresh, an external link, or a redirect that crosses live_session boundaries. An in-app live navigation (live_patch, or live_redirect/push_navigate/JS.navigate within the same live_session) issues no HTTP request and produces no dead render — it mounts straight into the connected state over the already-open socket, exactly once. There was never a double mount on a navigation to begin with, so resume has nothing to do there; it is purely a first-load optimization. (Easy to confirm: watch your server log while clicking an in-app link — you’ll see the LiveView’s queries and a diff reply, but no GET.)
This flips the authority. In stock LiveView the connected render is authoritative (it runs last, with connected?/1 true). Under resume, the dead render is authoritative for assigns — whatever it computed is what the live page shows. That single change drives every migration rule below.
Migrating to resume: four patterns
Flipping :resume on in an existing app is not free — code written for the stock lifecycle makes assumptions that no longer hold. Every assumption traces back to the same root: mount/3, the on_mount hooks, and handle_params/3 no longer run on the (initial) connect, so every if connected?(socket) branch inside them is skipped, and the dead render is now the source of truth. Here are the four patterns you’ll actually hit, in the order you’ll hit them.
1. Data loads: delete the connected? guard
The stock tip at the very top of this drop now works against you. if connected?(socket), do: load(), else: [] runs only on the dead render (where connected? is false), so it takes the empty branch — and the connect, which skips mount/3, never loads anything. The badge, the list, the page: empty, forever.
Under resume you delete the guard and load plainly. It runs once, on the dead render, and the parked socket carries the assign onto the connected process — one query, present in the first paint and when the page goes live. This is the payoff stock LiveView can’t give you: both branches of the earlier tradeoff at once.
# ❌ Stock pattern — under resume this loads nothing, ever
def mount(_params, _session, socket) do
posts = if connected?(socket), do: Blog.list_posts(), else: []
{:ok, assign(socket, :posts, posts)}
end
# ✅ Resume — load once on the dead render; it's reused on connect
def mount(_params, _session, socket) do
{:ok, assign(socket, :posts, Blog.list_posts())}
end
The same holds for param-driven loads in handle_params/3 — the place most apps actually load their main data (the :id lookup, search results, the paginated list). Because handle_params/3 is reused on the warm connect too (it ran on the dead render with the identical URL), you load there plainly as well: the timeline and the comments end up in the static HTML for SEO and are carried onto the live page with no second query. One caveat: if your handle_params/3 did connect-time work behind a connected?/1 guard — a push_patch that should only fire once live, say — that won’t run on the warm connect anymore. Move it to on_connect/1 (pattern #2).
2. Connection-only side effects: move them to on_connect/1
Subscriptions, timers, presence tracking — anything that must happen per live connection and must not happen on the dead render — can’t live in mount/3 anymore. The fork adds an on_connect/1 callback for exactly this. It runs once per WebSocket connect — cold, warm/resumed, and reconnect — and connected?/1 is always true inside it.
# ❌ Never fires under resume: the connected? branch in mount is skipped on connect
def mount(_params, _session, socket) do
if connected?(socket) do
MyAppWeb.Endpoint.subscribe("room:lobby")
:timer.send_interval(1_000, :tick)
end
{:ok, socket}
end
# ✅ on_connect/1 is the reliable home for connection-only work
def on_connect(socket) do
MyAppWeb.Endpoint.subscribe("room:lobby")
:timer.send_interval(1_000, :tick)
{:ok, socket}
end
The same applies to on_mount hooks that did connection-only work via connected?/1 — e.g. a test-only hook that allows the Ecto SQL sandbox for the connected process using get_connect_info(socket, :user_agent). Since the hooks don’t re-run on a warm connect, that allowance has to move to on_connect/1 too, or the connected process won’t have a database connection.
3. Values derived from connect params: move persisted state to a cookie
This is the subtle one. get_connect_params/1 returns data only once the socket is connected — it’s nil on the dead render. So any assign you derive from it is, by definition, missing on the dead render. In stock LiveView the connected render fixes it up. Under resume the hooks don’t re-run, so the bad dead-render value is the one that sticks.
This does not apply to get_connect_info/2, despite the symmetric-looking name: current LiveView exposes connect info during the disconnected render too (all keys are available there — it reads from the underlying Plug.Conn; on the connected render only the keys you declared in connect_info are). The trap here is specifically the client-supplied connect params.
The fix is to stop reaching for connect params at all and make the value available on the dead render, where it belongs. For persisted client state the server should know before rendering — a “show the welcome banner” flag, a theme preference, a dismissed-notice marker — that means a cookie, which rides along on the HTTP request the dead render is built from. Read it in a plug into the session, and your on_mount hook reads the session:
# ❌ get_connect_params is empty on the dead render → the banner flips on connect
# (a flash in stock LiveView; just plain wrong under resume)
def on_mount(:welcome, _params, _session, socket) do
show? = get_connect_params(socket)["show_welcome"] == "true"
{:cont, assign(socket, :show_welcome?, show?)}
end
# ✅ A plug copies the cookie into the session; the dead render reads it
plug :put_welcome_flag
defp put_welcome_flag(conn, _opts) do
conn = fetch_cookies(conn)
Plug.Conn.put_session(conn, "show_welcome", conn.cookies["show_welcome"] != "false")
end
def on_mount(:welcome, _params, session, socket) do
{:cont, assign(socket, :show_welcome?, Map.get(session, "show_welcome", true))}
end
No flash, no special-casing, and it’s correct under both stock and resume — because the value is available during the initial HTTP render, where the dead render can read it.
A value that only exists after JavaScript runs — a fresh viewport measurement, say — can’t be made available to that initial request retroactively. Handle that in on_connect/1 and accept a connected-only update, or persist it in a cookie for the next visit.
4. Real-time data: snapshot on the dead render, subscribe on connect
Some data is live — an unread-notification count, an online-users tally. There’s no fourth mechanism here; it’s patterns #1 and #2 combined. Load the snapshot once on the dead render so the first paint is correct (pattern #1), subscribe in on_connect/1 (pattern #2), and let PubSub increment it from there. The dead-render value is reused on the connect — you do not re-query it.
# Dead render loads the count (pattern #1, no connected? guard); reused on connect
def on_mount(:notifications, _params, _session, socket) do
{:cont, assign(socket, :unread, Notifications.count(socket.assigns.current_user))}
end
# On connect: just subscribe. New notifications arrive as messages and increment.
def on_connect(socket) do
Notifications.subscribe(socket.assigns.current_user.id)
{:ok, socket}
end
def handle_info({:new_notification, _}, socket) do
{:noreply, update(socket, :unread, &(&1 + 1))}
end
It’s tempting to also re-read the count in on_connect/1 “to be safe” — resist it. The dead render already has the authoritative value, and re-reading buys you only the hair-thin window between the dead-render query and the subscription, at the cost of a query on every connect for every user. If your domain genuinely can’t tolerate that window, re-read — but treat it as the rare exception, not the default. (And if a test seems to need the re-read, look closer: it’s usually a missing assert_receive on the broadcast, not a production gap.)
When to keep things in mount/3
A useful summary of the whole migration: under resume, sort each piece of work into one of three homes.
mount/3andon_mounthooks — pure, dead-render-derivable assigns: page data, the current user, the snapshot of a real-time counter, anything computed from params or the session. Runs once; reused.on_connect/1— connection-only side effects: subscriptions, timers, presence, the test sandbox allow.- A cookie/session read on the dead render — anything you used to pull from
get_connect_params/1.
If a value can be computed on the dead render, compute it there. If it can’t, ask whether it should be (and move its source to a cookie), or whether it’s a connection concern (and move it to on_connect/1).
Let your agent do the migration
The four patterns above are mechanical enough that you don’t have to apply them by hand. This whole post — the lifecycle table, the migration rules, the silent-failure checks — is written to be readable by a coding agent as much as by you. Point yours at the Markdown source of this drop and tell it to migrate your app:
Read https://elixirdrops.net/d/9mrw2kg3.md and migrate this LiveView app to the
:resume lifecycle: delete connected? data-load guards, move connection-only work
to on_connect/1, move connect-param-derived values to cookies, and flag anything
that needs a human decision.
It has everything it needs here: when each callback runs, where each kind of work belongs, and how to verify resume actually engaged afterward. Treat its output like any other migration PR — read the diff, run your tests — but the tedious part it can carry for you.
Verify it actually engaged — because it fails silently
This is the gotcha that cost me the most time, so it gets its own section. Resume has two halves: the server parks the dead-render socket and stamps a data-phx-resume token onto the root element, and the client reads that token and sends it back to redeem the parked socket. If the server half works but the client half doesn’t, nothing breaks and nothing warns you — the client simply ignores the token and does a normal cold mount. You get the double render back, the empty-then-full flash returns, and there is no error in the console, no error in the logs. It just quietly does the old thing.
The usual reason the client half is missing: your JS bundle doesn’t actually contain the resume client. Bundlers resolve a bare import "phoenix_live_view" however their module path is configured — esbuild’s NODE_PATH, for instance, can resolve a stale deps/phoenix_live_view (an older Hex copy with no resume code) before it ever sees the fork. The server runs the fork; the browser runs vanilla LiveView; they disagree in silence. A cached old bundle in the browser produces the exact same symptom after you think you’ve fixed it — so hard-refresh (or bust the digest) before concluding anything.
Three checks, server-outward, confirm it’s really on:
# 1. The dead render must stamp the token (server half is working)
curl -s http://localhost:4000/ | grep -o 'data-phx-resume="[^"]*"' | head -1
# -> data-phx-resume="SFMyNTY..." (empty = resume not enabled server-side)
# 2. The SHIPPED bundle must contain the resume client (client half is working)
grep -c 'data-phx-resume' priv/static/assets/js/app.js
# -> 1 (0 = your bundler resolved a phoenix_live_view WITHOUT resume;
# fix the dep/path resolution, rebuild, then hard-refresh the browser)
- Watch the server log on one fresh page load. With resume engaged you’ll see
mount/3and your data queries run once (the dead render), thenCONNECTED TO Phoenix.LiveView.Socketwith no secondmountand no second query batch. If you see the same queries fire twice — once for the GET, once right after the socket connects — resume isn’t engaging, and you’re back to step 2.
That third check is the real proof, because it measures the thing you actually care about: the work happened once, not twice.
A fair warning: this is an experimental fork, not a released package — you won’t mix deps.get it from Hex. It’s a stopgap we run while the official, more complete adoption mechanism (#3551) is designed upstream. If you take only one thing back to your stock-LiveView app today, take the connected?/1 guard at the top of this drop.
Don’t trust the fork — audit it, then fork it yourself
Running someone else’s fork of a framework in production deserves real scrutiny, so two diffs lay the whole thing bare:
main...resume— every source change:resumemakes against upstreamphoenix_live_view. This is the feature, in full.resume...resume-built— the-builtbranch apath/gitdep can point at without running an asset build. It’sresumeplus a single commit that checks in the compiledpriv/staticJS. The diff is nothing but generated assets — no extra source smuggled in, which is the point of keeping it as its own branch you can inspect.
And here’s the part I’d insist on if I were you: don’t depend on my branch. A branch I control can be force-pushed or quietly amended after you’ve read it, so what you audited today isn’t necessarily what you’d pull tomorrow. Read the diffs above, then fork it into your own account — or pin your dependency to the exact commit SHA you reviewed:
# Pin to the reviewed commit, not a moving branch — immune to anything I push later
{:phoenix_live_view,
github: "your-account/phoenix_live_view", ref: "<the-sha-you-audited>", override: true}
That way the resume feature you ship is the resume feature you read, byte for byte, regardless of what happens to my copy.
Links:
copied to clipboard