LiveView with complex layouts

That is exactly the layout I was going for.
My header is a menu that was put in the template and live-redirects to different pages.

The live pages then loaded two components, the side menu component with each menu item doing a live_patch with different actions and, depending on the live action, the component shown in the Main area.

Now I am running into the problem of setting the sign in/out button in the top menu cause I do not see how to access any information that i can use to determine if the user is logged in or not.

Why not put the layout inside the templates of the various page-specific LiveViews? You could write a simple function (or indeed a stateless LiveComponent) which takes HTML for the header, navigation and body as arguments and finally creates the constructed HTML combining all that in a full page from those arguments.

Usually when two modules have to have access to common things, they are going to be in a third module, either as sibling or parent. Just like phoenix context or generally module management. I believe liveview processes/states are managed the same way.

The article can also be applied (to live_render or to live_component). In backend system, fault tolerance makes more sense than GUI (sections). So make sure that on GUI, sections are really independent, more often they are coupled. When one section is crashed, the rest would need to refresh their stale presentation too.

I personally start with a root liveview process, and GUI sections are live_components, but what I write the most is def render_function(assigns), do: ~L"". Once I have @current_user tracked, I have to think twice why would I want to start fresh (live_redirect), instead of live_patch?. I write Elm at the same time and am used to thinking no local state or even “component” at all. Function is a good friend.

1 Like

So when defining routes that render inside your root liveview, do you point all those routes at the liveview but different actions?


I think I figured it out. You define your routes using the same liveview but with different actions. Then in handle_params you “look at” the incoming action and any url parameters and make a decision there on how to update the assigns on the socket.

In your (root) liveview’s template you just keep pushing down assigns to other components and liveviews.


Exactly, the mentality is the same as client SPA routing (see: Sections within a page barely need multiple processes. OP took some inspiration from Live Dashboad, but even Live Dashboard has only 1 process which is PageLive.

Given OP’s GUI layout, I have to ask myself why <main> needs a separate process instead of live_component with the following requirement:

Not sure if this is the need to access same set of state/truth or the need to use the same data structure which would be a share module in a phoenix context. If it’s the former case they should live in the same process, right?

render_header will also call a method on my child view.

I’d love to see what the exact method is, to understand more about the relationship between the header and the main.

1 Like

I’ve just played a bit with this idea.
Now that I’ve written this example code it feels almost trivial but for some reason this pattern didn’t click for me yet.

Here is a minimal example of how it could work:

defmodule LabsWeb.TestLive do
  use LabsWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    Process.send_after(self(), :update, 1000)
    {:ok, assign(socket, :counter, 0)}

  @impl true
  def render(assigns) do
      <%= live_component @socket, LabsWeb.Layout, header: &header/1, main: &main/1, counter: @counter %>

  def header(assigns) do
    header: <%= @counter %>

  def main(assigns) do
    main: <%= @counter %>

  @impl true
  def handle_info(:update, socket) do
    Process.send_after(self(), :update, 1000)
    {:noreply, assign(socket, :counter, socket.assigns.counter + 1)}

and the layout:

defmodule LabsWeb.Layout do
  use LabsWeb, :live_component

  def render(assigns) do
    <%= @header.(assigns) %>
    <%= @main.(assigns) %>

A disadvantage of this is that the layout needs to pass all the assigns to the respective components, but with some naming conventions this shouldn’t be an issue.
I don’t think there are other downsides or pitfalls to this (?) so I’m definitely going to explore this path more.


That’s not entirely correct if I understand this correctly. Here new LiveViews are rendered. Or doesn’t this start a new proces?

They need the same state, so I want them to live in the same process. I hadn’t discovered the pattern with assigning functions in components (see my previous post) so with this new knowledge the problem is basically solved.

As I see the PageBuilder is a plain module with callbacks that only look like liveview. Those callbacks get called under PageLive process (under its handle_*; those maybe_apply_module calls), maybe ask José to confirm.

A render function doesn’t require all assigns, try:

<%= header(Map.take(assigns, [:count]) %>

You can also do this

  def header(%{page: page} = assigns) do
    case page do
      :about -> render_about(Map.take(assigns, [:navs]))
      _ -> ~L""
  def render_about(assigns) do
      <%= for nav <- @navs do %>
        <%= nav %>
      <% end %>

ps; I didn’t test the above code, but i have written a bunch of those.

Finally view functions aren’t needed to be passed from a wrapper component as props (JSX mentality), view/render functions can be in a plain module (Just need the import Phoenix.LiveView, to use ~L sigil and have at least a assigns variable as parameter)

That’s not what the docs say:

Also there’s a macro that imports LiveView but haven’t looked at it in detail.

I agree, but like this the layout knows about what the header needs and that seems like terrible design and won’t scale.

Having header_assigns and pass those down is probably the way I would go.

The goal is that each LiveView can care about rendering the header content without having to care about the layout.

Yeah, looks like actions must be atoms. I guess we’ll have to stick to atoms such as :messages_index :messages_edit

My solution is definitely making me think of Elm now. I got things working for a while but then I bumped into the fact that you don’t have access to routing inside nested live views. (Views that aren’t mounted via the router.) I started experimenting with just one single live view at the root and using LiveComponent for anything else.

All my routes now point to a single live view:

# router.ex
live "/", PageLive, :index
live "/nutrition/", PageLive, :nutrition
live "/nutrition/:food_id", PageLive, :nutrition_detail

I do wish actions could be tuples. This would make it easier to pattern match within the hierarchy

For each sub-route, (In my case only :nutrition but more will follow), I define a state entry in the socket’s assigns and pass all messages and information to the corresponding sub-route module.

handle_info and apply_action both use pattern matching to select the correct module to handle the update:

# index.ex
defmodule FireweedWeb.PageLive do
  use FireweedWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    {:ok, socket |> assign(page: :index, nutrition: %{})}

  @impl true
  def handle_params(params, _url, socket) do
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}

  defp apply_action(socket, :index, _params) do
    |> assign(:page_title, "Home")

  defp apply_action(socket, action, params) when action in [:nutrition, :nutrition_detail] do
    FireweedWeb.Nutrition.Index.apply_action(socket, action, params) |> assign(page: :nutrition)

  @impl true
  def handle_info(message, socket) do
    case elem(message, 0) do
      :nutrition -> FireweedWeb.Nutrition.Index.handle_info(message, socket)
      _ -> {:noreply, socket}

In components I make use of phx-target to force component modules to handle events.

My main template pattern matches against @page (which is set in apply_action):

# index.html.leex
  case @page do
    :nutrition -> live_component(@socket, FireweedWeb.Nutrition.Index, id: "nutrition", nutrition: @nutrition)
    _ -> "Hello World"

I’m passing the .nutrition state to it so that updates to that slice will propagate.

In FireweedWeb.Nutrition.Index's mount I initialize state:

# /nutrition/index.ex
defmodule FireweedWeb.Nutrition.Index do
  use FireweedWeb, :live_component

  @impl true
  def mount(socket) do
     |> assign(
       nutrition: %{
         foods: [],
         food_id: nil,
         food: nil,
         query: ""

I’ll probably add a initial_state method that is called in the live view to initialise the .nutrition slice state. Since the live view passes updates using apply_action this is also defined in my sub module:

def apply_action(socket, action, params) do
    case {action, params} do
      {:nutrition_detail, %{"food_id" => food_id}} ->
        send(self(), {:nutrition, :fetch, food_id})
        socket |> assign(nutrition: Map.put(socket.assigns.nutrition, :food_id, food_id))

      _ ->

Only updating socket.assigns.nutrition using Map.put (I’ll make a helper function for that). For any expected messages handle_info is also defined as long as the first element in the tuple is :nutrition

def handle_info({:nutrition, :search, term}, socket) do

With the single live view in place I can route from anywhere in the hierarchy and now have 3 levels of routes with the nutrition sub module handling anything nutrition related be it routing, messages, or actions.

You can also pass extra data in the session, so you can use that as well

Guys, haven’t you considered use the PubSub? We use this approach in our projects without any problems. See below on gif image or here (Just try delete a patient and look at to the header count of patients.)

where we have sidemenu (and also header) as liveview:

# sidemenu_live.ex
  def mount(_params, _session, socket) do
    Phoenix.PubSub.subscribe(ServerWeb.PubSub, @topic)

    socket =
      |> assign(page: socket.root_view)
      |> assign(pages: pages())

    {:ok, socket}

  defp pages() do
      "settings" => %{
        "station" => ServerWeb.Settings.StationLive
      "graphs" => %{
        "actual" => ServerWeb.Graphs.ActualLive,
        "cells" => ServerWeb.Graphs.CellsLive,

# layout/sidemenu.html.leex
      <ul class="menu-list">
        <%= for {page, module_page} <- @pages["settings"] do %>
            <%= live_redirect to: Routes.live_path(@socket, module_page), class: is_active_menu_item(@page, module_page) do %>
              <span><%= String.capitalize(page) %></span>
            <% end %>
        <% end %>

And live layout:

# layout/live.html.leex
<p><%= live_flash(@flash, :notice) %></p>
<p><%= live_flash(@flash, :error) %></p>

<%= live_render(@socket, ServerWeb.HeaderLive, container: {:header, []}, id: "header") %>
<div class="hero is-fullheight-with-navbar">
  <div class="columns m-0">
    <aside class="column is-3 p-0">
      <%= live_render(@socket, ServerWeb.SidemenuLive, container: {:sidemenu, []}, id: "sidemenu") %>
    <section class="column is-9 p-0">
      <%= @inner_content %>

(Both apps are still WIP.)


I have thought of using PubSub yeah. The router-mounted live view could accept messages to call live_patch and send routing updates (actions, params) back to subscribers.

I’m still learning Elixir and Phoenix so I’m not 100% comfortable with it yet. My instinct is to try and solve this architecture by taking a compositional approach first similar to ReactJS and Elm and add PubSub for state management and sharing later when I have a better grasp of the language, framework and cost of introducing things like PubSub.

You can also pass extra data in the session, so you can use that as well

Those are only passed to mount though aren’t they?

It’s not for state management, but for message passing. There is nothing similar in React, where components would send messages to others directly.

It’s a bit like ReactContext or Redux no? The root live view “manages” router state and updates are communicated to (grand)child views via subscriptions?

Here You can send a message from everywhere, to anywhere… for example a child could send a message to the root, and vice versa.

And a message can be a state-transfer message, a notification message…

That’s why I would not compare it to Context, which is like a shared state between a hierarchy of components.

And somewhere, You can react to those messages, and change your state… à la Redux.

1 Like

It sounds like layouts will be added in future Live View, which will be great.

Meanwhile my code is setup like:

<html lang="en" class="has-navbar-fixed-top">
        <link rel="stylesheet" href="<%= Routes.static_path(@conn,"/css/app.css" ) %>">

        <!-- START NAV -->
        <nav class="navbar is-info is-fixed-top">
            <div class="navbar-brand">...</div>
        <section class="hero is-dark is-fullheight-with-navbar">
                <%= render @view_module, @view_template, assigns %>

Though, the title bar and such is stateless.