In a previous post I introduced Elixir, what it is and how you can use it. One of the places where I personally have seen great value in Elixir and its strengths is using it to add real-time support to an existing webapp. Either as a replacement for previous solutions based on periodic polling of an API or solutions with websockets from other frameworks.
The example we'll start with is a simple booking system, where a user can book a resource by opening it and making a booking that lasts from now until it's manually ended. To keep the example as simple as possible, there is no user management or backend, but everything is handled in the frontend with a static list of resources.
The system lists all resources
Confirmation when a user books a resource
In this app, the confirmation consists of a simple question, however it's common to require the user to fill in additional information about the booking. In these cases, the filling takes longer and it becomes more important to be able to show that the filling is in progress.
Why would you want to add real-time support? In this example it might seem unnecessary, but it's often the case that users expect to immediately see changes that other users make. This could be, for example, if the booking system is used by a large number of users or if it's some other type of system where multiple users interact with each other, for example if you want to be able to chat with each other from within the system.
You can find the skeleton of the system used on Github, where there are also instructions for how you can show the app locally if you want to follow along from your own computer.
The first thing we need to do is install Elixir and Phoenix. You can find
Elixir's official installation instructions
here. After that you install Phoenix, or
at least the tool needed to create a new project with Phoenix with
mix archive.install hex phx_new 1.5.6
. Now we're ready to create the Phoenix
project that we'll build on by running the command
mix phx.new realtime --no-ecto --no-html --no-webpack --no-gettext
This
creates a foundation for the project, which doesn't contain any database, any
frontend or any translations. In our example this is already handled by the
existing application.
Setting up the Phoenix server
Phoenix comes with built-in support for websockets through channels. A channel is a way to handle two-way communication between client and server. In our case, we want to create a channel that handles bookings of resources.
First, we need to create a channel module:
defmodule RealtimeWeb.BookingChannel do
use Phoenix.Channel
def join("booking:lobby", _params, socket) do
{:ok, socket}
end
def handle_in("book_resource", %{"resource_id" => resource_id}, socket) do
broadcast(socket, "resource_booked", %{"resource_id" => resource_id})
{:noreply, socket}
end
def handle_in("release_resource", %{"resource_id" => resource_id}, socket) do
broadcast(socket, "resource_released", %{"resource_id" => resource_id})
{:noreply, socket}
end
end
This channel handles three things:
- Joining the channel
- Booking a resource
- Releasing a resource
When a resource is booked or released, the channel broadcasts the change to all connected clients.
Connecting from the frontend
To connect to the Phoenix channel from our JavaScript frontend, we use the Phoenix JavaScript client:
import { Socket } from "phoenix";
let socket = new Socket("/socket", { params: { token: window.userToken } });
socket.connect();
let channel = socket.channel("booking:lobby", {});
channel.join()
.receive("ok", (resp) => {
console.log("Joined successfully", resp);
})
.receive("error", (resp) => {
console.log("Unable to join", resp);
});
// Listen for resource bookings
channel.on("resource_booked", (payload) => {
updateResourceStatus(payload.resource_id, "booked");
});
// Listen for resource releases
channel.on("resource_released", (payload) => {
updateResourceStatus(payload.resource_id, "available");
});
// Book a resource
function bookResource(resourceId) {
channel.push("book_resource", { resource_id: resourceId });
}
// Release a resource
function releaseResource(resourceId) {
channel.push("release_resource", { resource_id: resourceId });
}
Benefits of this approach
- Real-time updates: All users see changes immediately
- Scalable: Phoenix can handle thousands of concurrent connections
- Fault-tolerant: If a connection is lost, Phoenix automatically reconnects
- Easy to implement: The WebSocket handling is abstracted away
Conclusion
By adding Phoenix and Elixir to handle real-time communication, we can easily add live updates to an existing application without having to rewrite the entire frontend or backend. This gives users a much better experience and makes the application feel more responsive and modern.
The complete example code is available on Github if you want to explore further or use it as a starting point for your own real-time features.