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:
- front-end React code: shopper_umbrella/main.jsx at main · hubertlepicki/shopper_umbrella · GitHub
- 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
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
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 ![]()
As far as a “vision on web development” that is a big question, probably too much to fit a comment reply
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 ![]()
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
postskey,contentkey + 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
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.







