hubertlepicki

hubertlepicki

Client-side rendering, server-side state (StateChannel)

I have been thinking a lot about what are the pain points in web development, and what the remedy can be. First off, I’ve been programming web applications since about Rails 1.2, when the REST concept was introduced and was considered revolutionary, and I went through different stacks, that were both server-side, and client-side rendered: Dojo, Ember.js, React, Stimulus, LiveView, and many others…

Recently I was taking part in several projects using LiveView and one using Surface UI. I have got to say that both are great and definitely step in the right direction because they solve the problem of the biggest pain point in web development: creating and consuming the APIs. They solve it by cutting out the middleman completely, allowing you to fetch the data in Elixir, then, in a place where you’d have to build API and API client - do absolutely nothing! - and then render the template. Boom, instant productivity boost.

But LiveView is also not without it’s trade-offs, and a major one is the fact of being… Elixir library, and not JavaScript library, so the integration with JavaScript ecosystem (via Hooks or otherwise) is not always as easy and straight-forward as it’d be if I just rendered the templates in React. There are other issues, obviously, that I won’t mention here, but everyone who worked with LiveView on a fancy, JavaScript-heavy app knows this one so it’s what I mention.

Now, what if we reversed the situation? What if we kept React (or your framework of choice) and render the application client-side, in JavaScript, but we also kept the beloved feature of LiveView and not have to write or consume any API?

My idea is to put the state in the PhoenixChannel, and sync it with the client on the JavaScript side, whenever it changes.

The idea is that the client-side code emits events, that are being sent to Phoenix Channel as messages. Phoenix Channels library already is taking care of making sure the events are arriving and being processed in order in which they were emitted. Once they reach Phoenix Channel, they get consumed by programmer’s code and state is being updated, and synced back to client-side JavaScript app. The client-side app (say in React) reacts to state being updated and re-renders the parts of the application that changed.

I was experimenting in how to do that, and came across JSON diff and patch, as described in RFC 6902 - more info here: https://jsonpatch.com/.

There are libraries for both JavaScript and Elixir that can compute diff of arbitrary JSON structures, and apply patches. Patches are quite small and concise, and understand appending, inserting at position, replacing, and deleting parts of JSON/nested maps. It’s also relatively cheap to compute a diff, and apply a patch.

So, my thinking is that, the Elixir programmer would initialize state in the Phoenix Channel, to a map, map of maps and / or other basic types, and either update the map directly - resulting in the library that I created to wrap Phoenix Channels to calculate the diff - or - in case of large state and the need for speed - the Elixir programmer can produce JSON Patch operations themselves, and send these over the wire directly to the client-side app and apply patches locally on the server-side, so both states stay in sync.

The relevant Elixir code that does the above, and wraps around Phoenix Channels can be found in this file state_channel/state_channel.ex at main · amberbit/state_channel · GitHub

On the JavaScript side, I chose React, but it’s possible to create bindings for pretty much anything. Anyway, on the React side, we have a library that delivers Providers for Channel and State, allowing for the React components to just consume the state, without worrying about it’s synchronization, and allowing for events to be sent to the backend. The relevant file with JavaScript code is here: state_channel_react/index.jsx at main · amberbit/state_channel_react · GitHub

Now, how would a programmer use it? Let’s start with a React application. At first, the programmer needs to wrap his or her components into a provider, that connects to the specified channel and listens to the state changes, like this:

    <PhoenixSocketProvider>
      <StateChannelProvider topic="app:state">
        <MyComponent />
      </StateChannelProvider>
    </PhoenixSocketProvider>

Within the component, you can consume the state, pass it whole or as props to child components, and emit events:

const MyComponent  = () => {
  const {state, pushMessage} = useStateChannel();

  return(
    <div>
      <h4>Current state is:</h4>
      <pre>
        { JSON.stringify(state, null, 2) }
      </pre>

      <a href="#" onClick={(e) => {e.preventDefault(); pushMessage("change_state")}>Change state!</a>
    </div>
  )
};

The attached click handler sends message “change_state” to the server.

Now, in our channel, responsible for handling “app:state” topic, we can react to the state being changed, by setting a new state. The state diff will be then computed, and JSON diff structs will be transfered over the wire back to the client, and it will re-render using the new values.

defmodule MyApp.AppChannel do
  use StateChannel

  @impl Phoenix.Channel
  def join("app:state", _message, socket) do
    {:ok, socket |> assign(:state, init_state())}
  end

  @impl Phoenix.Channel
  def join(__otherwise, _params, _socket) do
    {:error, %{reason: "unauthorized"}}
  end

  @impl StateChannel
  defp init_state() do
    %{
      "greeting" => "Hello, World!"
    }
  end

  @impl StateChannel
  def on_message("change_state", value, socket) do
    socket
    |> set_state(%{"greeting" => "Hello, Elixir!"})
  end
end

Again, this works with small state well, for large state you can use patch_state with JSON path that will update the server side state in place, and send the diff - that doesn’t have to be computed - to the client:

  @impl StateChannel
  def on_message("change_state", value, socket) do
    socket
    |> patch_state("/greeting", "Hello, Elixir!"})
  end

Since state updates happen exclusively on the server, in Channels, this approach can be used to build collaborative applications similar to LiveView, or listen to some server-side events and update state, or even components on the same page can communicate using OTP messages or PubSub to interact behind the scenes - and when state of either of the components gets updated on the server - it gets synced to clients.

I have put together a little bigger demo and deployed to fly.io:

https://react-phoenix-server-state.fly.dev/

The source for the app is here:
GitHub - hubertlepicki/shopper_umbrella: An example application to demonstrate StateChannel + React and releavnt files are:

  1. front-end React code: shopper_umbrella/main.jsx at main · hubertlepicki/shopper_umbrella · GitHub
  2. back-end Channel code:
    shopper_umbrella/app_channel.ex at main · hubertlepicki/shopper_umbrella · GitHub

The sources for React and Pheonix libraries used by the app above are here:

The code itself is a proof-of-concept quality, but, for the task of the demo app - it seems to work just fine.

What do you folks think of this approach? I am sure there is a thousand concerns, and things I didn’t consider, and I would appreciate suggestions and criticism.

Marked As Solved

hubertlepicki

hubertlepicki

Nice finds, taper seems abandoned, however. LiveState seems like another very close idea to mine! In fact it looks very similar, and the author even uses Json Diff / Patch approach!

Also Liked

superchris

superchris

The original impetus for creating LiveState was that I wanted to see if I could use LiveView to build an embedded or third party app: something you could drop into a web page hosted somewhere else, like a comments section or shopping cart. My first attempt was to see if I could render a LiveView into a custom element, but this proved difficult technically and less appealing the deeper I got.

My next thought was: What if I did something like LiveView, but kept only the rendering on the client. This is what led me to LiveState. I realized that the core of LiveView is the pattern I named (for lack of an existing name) the Event State Reducer pattern: functions which take an event, the current state, and return a new state. The idea of LiveState is to implement this same pattern but keep rendering client side: I push events up over the channel, and push state back down. It started as an experiment to “see how it felt” to build applications this way. My experiences and the feedback from others seemed good, so I kept going, and here we are :slight_smile:

As far as a “vision on web development” that is a big question, probably too much to fit a comment reply :slight_smile: I’m very interested in continuing to explore the embedded app problem space, but whether the LiveState approach is a good approach to web app development more generally is an interesting question. As always, the answer is almost certainly: it depends.

I’ve been a proponent of web components for many years, so my intention is to support custom elements in a “first class” way while making it as easy as possible to support other frameworks. My philosophy, is as much as I have one, is to use the capabilities provided by the browser as much as I am able and avoid anything that locks me into a specific framework. I continue to be amazed by how much I see frameworks invent their own solutions to problems already solved by web standard APIs, but that is another rant for another day :slight_smile:

hansihe

hansihe

I want to throw my project into the arena here as well. Confusingly it’s also called LiveData, but uses a quite different approach from a lot of other projects I have seen.

LiveData has a concept of tracked functions, defined with deft instead of a regular def. When compiling deft functions, they are transformed in a way which extracts static chunks of the data structure and sends them only once. Instead of a general tree diff, it draws inspiration from virtual dom frontend frameworks and enable you to specify keys/IDs for subtrees which enables the runtime to efficiently generate diffs for you.

  deft render(assigns) do
    %{
      posts: for post <- assigns.posts do
        keyed post.id do
          %{
            id: post.id,
            title: post.title,
            content: post.content,
          }
        end
      end
    }
  end

This is how the render function could look like in a LiveData. Other than this, things look pretty much exactly like a LiveView.

Here, keyed is used to specify an a key that can uniquely identify the post. By doing this you get the following advantages:

  • If a post changes, only the change in that post is ever sent.
  • No static content is sent twice, the posts key, content key + more in the map is static, so is only sent once at the beginning of the connection.
  • If a post is added, only the added post is sent.

It’s not very cleaned up, but here is a simple example of a LiveView: phoenix_data_view_demo/hello_world_data.ex at main · hansihe/phoenix_data_view_demo · GitHub

methyl

methyl

I’m the author of tha live_data where I explored this idea, thanks for the mention @Sebb.

I still think it’s an interesting direction. The most problematic thing in this approach (true for both LiveData and LiveView) is that it relies on network to apply local state/UI changes. For some cases it’s not a big deal, for example if we are building a big interactive dashboard when most of the changes are going on on the server. For building apps in general, I think the ability to apply mutations locally is a must, that’s why I was never a big fan of LiveView.

Since then, I’ve discovered Replicache and watched its development closely. It builds on the idea of holding the state on the server as a single source of truth and pushing diffs to clients, but it also allows clients to apply mutations optimistically with casual consistency which results in a much better user experience.

While Replicache is (as of now) designed mostly around JS ecosystem, it’s in its core backend agnostic. I think exploring how well it will play together with the Elixir Way is worth looking into, I have a feeling it’s a great fit.

Where Next?

Popular in Discussions Top

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
Donovan
Hello everyone, I’m so glad to have discovered this awesome community. Thanks for creating it! This is my second post, and apologies for...
New
Qqwy
Looking at the stacks that existing large companies have used, WhatsApp internally uses Mnesia to store the messages, while Discord uses ...
New
IVR
Hi all, I’ve seen a number of related threads in the past, but I’d still be very curious to hear an up-to-date opinion on this topic. I...
New
mmport80
I have put far too much effort into Dialyzer over the last year or so - and basically - I doubt it’s worth the effort. It’s not as easy ...
New
New
shishini
I think this twitter post and youtube video didn’t get as much attention as I hoped I am still new to Elixir, so can’t really judge ...
New
AstonJ
Please see the new poll here: Which code editor or IDE do you use? (Poll) (2022 Edition) It’s been a while since we first asked this, I...
208 31107 143
New
hazardfn
I suppose this question is effectively hackney vs. ibrowse but we are at a point in our project where we have to make a choice between th...
New
kostonstyle
Hi all How can I compare haskell with elixir, included tools, webservices, ect. Thanks
New

Other popular topics Top

skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30840 112
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
vegabook
I'm brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
New
nobody
Hi! In PHP: $SERVER['SERVERADDR'] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
klo
Got a question about when to concat vs. prepending items to list then reversing to achieve appending. So i know lists boil down to [1 | ...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New

We're in Beta

About us Mission Statement