|
| 1 | ++++ |
| 2 | +title = "Elixir: Having multiple Live Views on the same page" |
| 3 | +description = "How to leverage Live Session for complex structures" |
| 4 | +date = 2025-07-25 |
| 5 | +[taxonomies] |
| 6 | +tags = ["tutorial", "elixir", "phoenix", "liveview"] |
| 7 | ++++ |
| 8 | + |
| 9 | +## Context |
| 10 | + |
| 11 | +In its early versions and up until v1.0, Phoenix LiveView has been designed to let you manage one LiveView at a time. But in a few cases, you'll need to be able to manage multiple Live Views, for either a complex dashboard, or even a navigation menu living on the side. |
| 12 | + |
| 13 | +In my case, I had to implement a navigation bar in one of my projects, with a different class for the active element. Using a Live Component was not possible, because those can't be called from a regular (non-Live) template. |
| 14 | + |
| 15 | +Although, since Live Views are GenServers, and therefore Processes, having one Process per user constantly running just for displaying a dynamic navigation bar isn't great. It would be much better to save those few kilobytes of RAM, and do the work with pure CSS and JS hooks, therefore offloading the logic to the client. But let's just consider we need two LiveViews sitting next to each other for our use case. |
| 16 | + |
| 17 | +This is where [`live_session/3`][1] comes into play. |
| 18 | + |
| 19 | +## Usage |
| 20 | + |
| 21 | +`live_session/3` is a Router-specific "word" (in the Router DSL) that lets you define a handful of things: |
| 22 | +* A `name` to identify it |
| 23 | +* A `session` argument, in case you want to put a value in the connection (HTTP) Session |
| 24 | +* A `root_layout` argument, in case you want to override it (it may already be specified in your pipeline) |
| 25 | +* A `layout` argument, to specify a "partial view" that'll be rendered inside your root layout |
| 26 | +* An `on_mount` callback to specify a list of functions to call in order to update the socket |
| 27 | + |
| 28 | +## Example |
| 29 | + |
| 30 | +This is how my code looks like: |
| 31 | + |
| 32 | +```elixir |
| 33 | +# router.ex |
| 34 | +live_session :admin, |
| 35 | + on_mount: {SutoWeb.InitAssigns, :admin}, |
| 36 | + layout: {SutoWeb.Layouts, :"live.admin"} do |
| 37 | + scope "/admin", SutoWeb.Admin do |
| 38 | + pipe_through :admin |
| 39 | + live "/", IndexLive |
| 40 | + end |
| 41 | +end |
| 42 | +``` |
| 43 | + |
| 44 | +```elixir |
| 45 | +# init_assigns.ex |
| 46 | +defmodule SutoWeb.InitAssigns do |
| 47 | + import Phoenix.Component |
| 48 | + |
| 49 | + def on_mount(:admin, _params, _session, socket) do |
| 50 | + current_page = |
| 51 | + case Atom.to_string(socket.view) do |
| 52 | + "Elixir.SutoWeb.Admin.AuthLive" <> _rest -> |
| 53 | + :auth |
| 54 | + |
| 55 | + "Elixir.SutoWeb.Admin.ChannelLive" <> _rest -> |
| 56 | + :channels |
| 57 | + |
| 58 | + "Elixir.SutoWeb.Admin.TimerLive" <> _rest -> |
| 59 | + :timers |
| 60 | + |
| 61 | + ... |
| 62 | + end |
| 63 | + |
| 64 | + {:cont, assign(socket, :current_page, current_page)} |
| 65 | + end |
| 66 | +end |
| 67 | +``` |
| 68 | + |
| 69 | +```elixir |
| 70 | +# Partial layout |
| 71 | +<div class="min-h-screen h-full flex flex-row bg-gray-300"> |
| 72 | + <.live_component |
| 73 | + module={SutoWeb.Admin.NavLive} |
| 74 | + id="nav" |
| 75 | + class="p-5" |
| 76 | + current_page={assigns.current_page} |
| 77 | + /> |
| 78 | + <main class="p-5 w-full h-screen overflow-auto"> |
| 79 | + <.flash_group flash={@flash} /> |
| 80 | + {@inner_content} |
| 81 | + </main> |
| 82 | +</div> |
| 83 | +``` |
| 84 | + |
| 85 | +And the `NavLive` module here is just a navigation bar built with [Petal Framework][2], defining list items and picking the right Tailwind class for the active item. |
| 86 | + |
| 87 | +That's it :) Please keep in mind that using a Live Session for something that could be offloaded to the client (with JavaScript and hooks) isn't great, but this gives you an idea on what structure to use, in case you're building a complex trading dashboard, and you absolutely need to have separate LiveViews on the same page, for concurrency reasons or resilience. |
| 88 | + |
| 89 | +[1]: https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Router.html#live_session/3 |
| 90 | +[2]: https://petal.build/ |
0 commit comments