Qqwy

Qqwy

TypeCheck Core Team

LiveView: shallow states or deep states?

I’ve been building some projects using Phoenix LiveView, and have read the source code of quite a few more projects made by other people (thanks, Phoenix Phrenzy contest!).

One thing I am seeing, is essentially a dichotomy in two approaches to working with state and interactivity, when working with LiveView-powered applications that are essentially ‘single-page’ (i.e. rather than one or multiple pages with multiple independent LiveView snippets, the whole interface is driven by LiveView).

Approach 1: “Shallow State”

In this approach, we have many nearly-independent LiveViews/ (stateful) LiveComponents that mostly work with their own socket.assigns directly.

Because socket.assigns is a map, and because all of these components are stateful, the data that is stored and the communication between them is mostly ad-hoc, i.e. non-normalized.

Approach 2: “Deep State”

In this approach, we have a single LiveView with potentially multiple nested (stateless) LiveComponents that contains the state in a (deeply) nested map in its socket.assigns. Most or all changes that come in travel through this single LiveView’s handle_event and potentially update (some nested part of) its socket.assigns. The socket.assigns consists of one or a few Elixir Structs, which makes it very clear what kind of data to expect, i.e. the data is normalized.

Using this approach, it is possible to extract most of the domain-logic out to its own separate module(s), making the code easy to test. Of course, you do end up with more code since you add a/some layer(s) of abstraction between the LiveView-module and the place where the actual state-transition logic happens.

This kind of architecture has strong similarities with first-order functional reactive programming (also known as ‘the Elm Architecture’(TEA)).


Now, I don’t know which approach is better suited for what kind of situations, but I find it very interesting to see that there are these two sort-of opposed kinds of working with LiveView going on, and I’d love to start a discussion on the pros and cons! :grinning:

Most Liked

Qqwy

Qqwy

TypeCheck Core Team

During @chrismccord’s Keynote-talk at ElixirConf.EU V yesterday he told us about the new changes and additions that are happening to LiveView.

One important thing he mentioned, is that there will be better diffing-support for deeply-nested states. Previous versions of the LiveView assigns-diffing would not be able to look deeper into the keys of nested maps (and especially inside structs), meaning that code like this:

<div>
  <span> <%= @post.author %> </span>
  <p> <%= @post.content %> </p>
</div>

would be re-evaluated as a whole every time one of the fields inside assigns.post would change. This is now (in the newest version on master; the latest kinks were ironed out during these last couple of days) no longer the case.


So the tl;dr: Deep states will be much better supported going forward, which means that we as developers have more flexibility in the approach we take.

chrismccord

chrismccord

Creator of Phoenix

Deep diffing shipped many weeks ago :slight_smile:

mbuhot

mbuhot

I’m just learning LiveView through the PragStudio course which is excellent so far.

The temporary_assigns feature makes it feel like the framework is pushing users towards the shallow state style. Can you indicate that a nested map key is a temporary?

I found it difficult to track which assigns are available within the template without some declaration of the data structure, so I’ve adopted a convention of an inner Assigns module within the LiveView module that declares a @type and provides functions for managing the state:

Example
defmodule MyApp.MyViewLive do
  use MyApp, :live_view

  alias MyApp.SomeContext
  alias MyApp.SomeContext.SomeSchema

  defmodule Assigns do
    @type t :: %{
            items: [SomeSchema.t()],
            filter: String.t()
          }

    @spec new([SomeSchema.t()]) :: t()
    def new(initial_data) do
      %{
        items = initial_data
        filter = ""
      }
    end

    def temporaries() do
      [items: []]
    end

    def with_filters(socket, items, filter) do
      socket |> assign(items: items, filter: filter)
    end
  end

  @impl true
  def mount(_params, _session, socket) do
    initial_data = SomeContext.get_some_data()
    socket = assign(socket, Assigns.new(initial_data))
    {:ok, socket, temporary_assigns: Assigns.temporaries()}
  end

  def render(assigns) do
     ~L""" access @items etc here """
  end

  def handle_event("some_event", %{"filter" => filter}, socket) do
    data = SomeContext.get_some_data(filter)
    socket = socket |> Assigns.with_filters(data, filter)
    {:noreply, socket}
  end
end

For the relatively small examples I’ve followed so far, this has been helpful to allow me to treat the Socket.assigns map as a data structure with well defined operations. Looking forward to some more complex examples with Components :slight_smile:

Where Next?

Popular in Discussions Top

PragTob
Hello everyone, I know we had quite some threads (read through lots of them) about background job processing but it remains a hotly deba...
New
jeramyRR
This is an interesting article to read. Elixir’s performance, like usual, is excellent. However, it seems like the high CPU usage is co...
New
thojanssens1
It would be nice to be able to define a redirect from one route to another from the router.ex file. E.g.: redirect "/", UserController, ...
New
arpan
Hello everyone :wave: Today I am very excited to announce a project that I have been working on for almost 3 months now. The project is...
New
ricklove
I was just introduced to Elixir and Phoenix. I was told about the 2 million websocket test that was done 2 years ago. From my research, t...
New
crabonature
I’m still quite new to Elixir. As I understand we got in Elixir “multi guards” as convention to simplify one large guard with or’s?: de...
New
WolfDan
After doing a port from a c++ library to my project in phoenix I’ve seen that I need a faster way to run this algorithm and I found this ...
New
sergio
There’s a new TIOBE index report that came out that shows Elixir is still not in the top 50 used languages. It also goes on to call Elix...
New
CharlesO
Erlang :list.nth simple, but 1 - based nth(1, [H|_]) -&gt; H; nth(N, [_|T]) when N &gt; 1 -&gt; nth(N - 1, T). Elixir Enum.at … coo...
New
cblavier
Hey there, It’s been more than a year since we started using LiveView as our main UI library and building a whole library of UI componen...
New

Other popular topics Top

marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 53578 245
New
AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

We're in Beta

About us Mission Statement