Web apps have client and server state (plus realtime and LiveView)
Recent discussions around client-side and server-side frameworks have brought up some misconceptions about state handling in web applications. This article aims to address them with some examples, and go a bit more into detail about how LiveView, in particular, deals with state and realtime.
Server state
Most web applications have state on the server in the shape of the database (at least). If we assume a web application that is not sending updates to the client as they happen, as soon as you render the page in the browser, the page you are seeing may be out of date. Someone may have added a new comment to a blog post or even the author has updated it with an errata. Many times, telling the user to refresh the page to get the latest version is fine, other times, you may want to update the page in (web) realtime.
For example, if you have an application called “hot sales”, which sells a limited amount of items with discount prices every hour, you probably want to update the availability of items as they change.
Even then, it is still important to understand that the client information is always out of date. Even if the user just got a server update saying that a few items are available, and the user immediately clicks on “Buy”, there is no guarantee this was successful without an acknowledgement of the server.
This has implications on both client and server code, regardless if client-side or server-side. For example, if you decide to add optimistic UI updates to this experience, you probably don’t want to say “You bought it” while you wait for the server. Telling a user they bought something, and then soon after tell them that they actually did not, could be quite frustrating. Instead your optimistic UI should stick with something more neutral, such as “We are reserving your product”.
The “hot sales” web application also requires care on the server and database sides. Since most web applications have multiple servers talking to a single database, you may have data races. For example, if you write this pseudo-code on the server:
if (product_available?(product)) {
product_sold_to(product, user)
}
It may happen that, between checking the product is available and selling it, it may have already sold out. You need to use atomic operations, transactions with isolation levels, and similar to tackle these problems correctly.
While not all problems are this complex (thankfully!), there is a lot we could discuss and there isn’t a one size fits all solution. The important to keep in mind for now is that, if you have data in a web application that can be read and updated by different actors, you need to consider the data/page in the client may be out of date.
Client state
So far we have talked about the client state from the point of view of it being out-of-date, compared to the server state. However, the majority of web applications also have its own client state, even if they don’t include a single line of JavaScript.
The simplest client state we can think about is a form with inputs. Imagine you wrote a blog post and now you want to edit it. As soon as you change any character in the post, you have state in the client that has never been seen before by the server. And, once again, depending on the application, you may want to carefully consider the implications of client state, regardless if you are using a client-side or server-side framework.
For example, it could be a disaster if two people are editing the same blog post at the same time, and then the changes of one of them end-up immediately erasing the other’s. To deal with this, you may want to version all changes to blog posts. Another simple approach is to use optimistic locks: when you open up a post for edition, you include its version in the form. When the form is submitted, you check if the version is still the same, if so, you proceed, if not, you tell the user they were not working on the latest version (as done by git
). If possible, if you can detect in realtime someone else is editing the page (which would be trivial in Phoenix with its multi-party presence mechanism), then you can let the user know about potential conflicts quite early on.
Once again, there is a lot to explore here! For text editing, you could use collaborative editors or explore local-first principles. The overall idea is that clients have their own state (for offline apps this state changes even when disconnected) and, at some point, this state needs to be synchronized with the server (or other clients). To the server, it doesn’t really matter if the state is a consequence of typing into an input or dragging and dropping an item.
Enter Realtime
Given that web applications have both client and server state, client-side and server-side frameworks need to deal with both. State is an intrinsic part of the problem, therefore it must be an intrinsic part of the solution. Ignoring how state changes on the server or ignoring how state changes on the client will lead to poor user experiences. Restricting a stack from leveraging any of these states would be an artificial limitation that does not benefit anyone who wants to write production applications.
It is also worth saying that, even if you can share programming languages between client and server code (such as JavaScript, Clojure, Gleam, etc), which is definitely a plus, they are often working on fundamentally different problems. The client needs to concern itself with DOM manipulations, syncing the latest state from the server, etc. While the server needs to conciliate changes from multiple clients, perform authorization, guarantee data consistency at the database level, and more.
With this in mind, let’s talk about realtime.
For simplicity, let’s consider a realtime web application to be one that where the server sends updates to the client at will, as those updates happen on the server, within a reasonable time (a few seconds but not minutes). Not all features in an application need to be realtime for the purpose of this discussion.
Let’s go back to the blog post editor example. Using a collaborative editor would be a fantastic feature to add and improve the user experience: changes across users are now automatically synchronized. However, what happens if two users also change the category of a blog post while editing?
Your application likely uses a select or a dropdown to choose categories. While you could automatically synchronize changes to the dropdown across users, is that the best user experience? How would the user feel if they see the value of the dropdown changing out of nowhere, with no explanation? What happens if they both change the dropdown at the same time? What happens if a user changes it while the other has the dropdown open?
A possible solution is to show a message or an indicator that the data has been changed by someone else, until these changes are effectively published. It is a simple user interface solution for a problem with technical roots.
The point I want to drive here is that realtime does not necessarily mean “override the client with the latest version”. It doesn’t matter if the server is sending updates to the client in the shape of data (as client-side frameworks typically do) or as chunks of HTML (as server-side frameworks typically do), you need to consider how realtime updates are shown to clients.
Hello LiveView!
Phoenix LiveView is a framework for writing rich realtime and interactive applications with server-rendered HTML written in Elixir. Since LiveView runs on the server, it naturally addresses server state and stays close to the database. But it also has abstractions for dealing with client state (it is a must!). With an understanding of these concepts, we can now discuss some of the implementation details within LiveView.
We have just discussed that realtime applications need to consider how updates are shown to the client. Because LiveView keeps a open WebSocket/Longpoll connection between the client and the server, LiveView is aware and controls what the client has rendered, and therefore developers can decide how realtime updates are propagated and rendered within LiveView itself in most cases. LiveView also does a lot of work behind the scenes to guarantee those updates are cheap and performant. Without a persistent connection, resolving realtime updates on the server becomes riskier, because it increases the chance you will overlap with something the client is doing or seeing.
However, we have just talked about how applications have client and server state. If there is client state, how can LiveView manage realtime updates? In such cases, because the state is in the client, we must resolve it on the client. For such, LiveView provides a client-side mechanism called Hooks, which integrates the LiveView lifecycle with the DOM, allowing developers to react to connections, disconnections, and updates in whatever way they prefer. And the LiveView team has mentioned, as part of their v1.0.0-rc announcement, that integration with WebComponents is on the roadmap too.
Here is the interesting part: because LiveView was designed to tackle realtime problems, it has a strong foundation for dealing with optimistic updates too, because ultimately they are both about synchronizing client state with server updates. You can consider an application with optimistic updates to be a subset of a realtime one, since you only receive updates caused by your own actions. To make this practical, we had to implement one mechanism within LiveView. Let’s talk about it.
The need for clocks
Imagine your web application has a form. The form has a single email input and a button. We have to validate that the email is unique in our database and render a tiny “✗” or “✓“ accordingly close to the input. Because we are using server-side rendering, we are debouncing/throttling form changes to the server. And, to avoid double-submissions, we want to disable the button as soon as it is clicked.
Here is what could happen. The user has typed “hello@example.” and debounce kicks in, causing the client to send an event to the server. Here is how the client looks like at this moment:
[ hello@example. ]
------------
SUBMIT
------------
While the server is processing this information, the user finishes typing the email and presses submit. The client sends the submit event to the server, then proceeds to disable the button, and change its value to “SUBMITTING”:
[ hello@example.com ]
------------
SUBMITTING
------------
Immediately after pressing submit, the client receives an update from the server, but this is an update from the debounce event! If the client were to simply render this server update, the client would effectively roll back the form to the previous state shown below, which would be a disaster:
[ hello@example. ] ✓
------------
SUBMIT
------------
This is a simple example of how client and server state can evolve and differ for periods of times, due to the distance between them, independent of the stack.
To address this in LiveView, whenever it pushes an event to the server, LiveView includes a unique identifier, built from an always increasing counter (aka the clock), and ties this identifier to stateful elements (your client state). Then LiveView proceeds to queue the server changes to these elements until it receives an update that matches the most recent identifier. Building rich experiences would be impossible without LiveView being aware of client state.
The collection of these features is what makes it possible for a LiveView application to broadcast drag and drop events to all users in realtime while providing a nice user experience even on 3G connections.
Next steps and considerations
Today LiveView has two main interoperability mechanisms with the client: hooks, that integrate LiveView’s lifecycle events with DOM elements, and commands, which perform common client-side actions without the server round trip.
The clock mechanism described in the previous section is used internally for several LiveView features and it is partially exposed to users via the :loading
option on push: the loading option applies and removes classes from elements, which can then react to these events. In the simplest cases, you can simply use CSS to customize a given element while it waits for the server update. When using Tailwind, Phoenix includes class variants to make this trivial.
We are also discussing mechanisms to expose our clock programmatically, making it easier to pre-render a component on the client, while waiting for the server ack. Note this is already possible with hooks today and projects like LiveSvelte can help avoid duplication in the more complex cases.
At the end of the day, most applications will require some JavaScript integration. As an example, an application such as LiveBeats, which allows users to start their own radio station for people to listen to in realtime, requires 340 lines of JavaScript application code.
While Phoenix LiveView does not support all use cases, the most obvious being offline mode, and while it will still requires you to bring JS libraries for managing the DOM, it packs quite a punch! You get realtime messaging, multi-party presence, client-side interoperability, fast browserless testing, uploads, monitoring dashboards, and much more! All provided out-of-the-box, without the need for third-party dependencies or 3rd party service providers. And that’s without taking into account everything else you get from the Elixir and Erlang ecosystems.
Phoenix LiveView is currently in release candidate for its upcoming v1.0 release, give it a try!