Svelte is a rapidly-increasing-in-popularity JavaScript framework for building web applications. It exists in a similar place to React
and Vue
- and approaches similar problems - "how can we make modern, reactive, web applications that are both easy to use, and easy to maintain".
Contrary to the two dominent frameworks, Svelte
approaches this problem from another angle, from their own docs:
Svelte is a radical new approach to building user interfaces.
Whereas traditional frameworks like React and Vue do the bulk of their work in the browser,
Svelte shifts that work into a compile step that happens when you build your app.
Instead of using techniques like virtual DOM diffing, Svelte writes code that surgically
updates the DOM when the state of your app changes.
While not as popular as the behemoth that is react
, Svelte
is a rapidly growing with around ~60k stars on GitHub
and is used in 97k projects.
Svelte
is a great tool for building web applications, but as with all of the modern SPA
-style frameworks, we need to make sure we use the Ably JavaScript SDK
effectively and play nicely with the reactive rendering built into the framework.
While svelte
is a compiler that generates vanilla JavaScript, it shares similarities with the other React
-style frameworks. Svelte
apps are all reactive by default, so we have to make sure our JavaScript SDK - which maintains open websockets connections to the Ably service - is used effectively.
To do this:
- We need to handle component mounting and unmounting
- We need to make sure our Ably code doesn't run on the server, where it makes no sense
- We need to make sure we correctly handle subscriptions and channel detachments, so we don't leak resources and use up our messaging limits
In this demo, we're going to use Ably
to build a real-time application that allows us to send messages to a channel, and then display those messages in the UI.
The app will do the following things:
- Display messages received from the channel in a UL element
- Send a message to the channel when the user clicks a button
It's a lo-fi demo, but it contains all the moving parts of any other real-world app.
Svelte
has a quickstart guide if you've never used the framework before, but in brief, you can use Svelte
in two different ways:
- You can experiment in the Svelte REPL playground and download your app when you're ready
- You can use
npx
(which is bundled with npm) to run a tool calleddegit
.
npx degit sveltejs/template my-svelte-project
cd my-svelte-project
npm install
npm run dev
degit
clones a template repository from GitHub
, and executing the commands above in a terminal
will leave you with a working Svelete Hello World app.
We need one additional dependency to start building our Ably
integration - the ably
package from npm
. Install the package from your terminal:
npm install ably --save
For the sake of demo we're going to use our Ably API key in our markup - in a real app, we'd need to get this from an API to do token authentication
, and if we were using SvelteKit
as an application framework, we'd want to create an Endpoint
to manage our tokens.
Let's open the src/App.svelte
file and replace its contents with this:
<script>
let messages = [];
</script>
<div class="App">
<h1>Ably Svelte Demo</h1>
<div>
<button>Send Message</button>
</div>
<h2>Messages</h2>
<ul>
{#each messages as msg}
<li>{msg.data.text}</li>
{/each}
</ul>
</div>
We can now launch our app with npm run dev
npm run dev
And our app will be served on http://localhost:5000/
- and you'll see the UI.
We're going to add some code to subscribe to an Ably channel, but we need to make sure we only execute our code in the DOM - once our application is mounted in a browser. Svelte
implements lifecycle functions
that let us hook into the mounting and rendering process - and we're going to use one called onMount
- you can read the offical docs if you'd like a deeper understanding.
onMount
executes code when our component mounts, and if you return a function from your onMount
callback, it'll run that when the component unmounts or updates.
<script>
import { onMount } from "svelte";
let messages = [];
onMount(() => {
// code to run when component mounts
return () => {
// code to run when component unmounts
};
});
</script>
We're going to expand out our onMount
call to initilise the Ably JavaScript SDK
, and return an instance of a channel
. We're going to save this channel instance
to a variable called channel
as we'll need it later.
<script>
import { onMount } from "svelte";
import * as Ably from "ably";
let messages = [];
let channel = null;
onMount(() => {
const ably = new Ably.Realtime.Promise({
key: "your-api-key-here",
clientId: "someid",
});
channel = ably.channels.get("your-channel-name");
return () => {
// code to run when component unmounts
};
});
</script>
Once we have our channel instance, we can both publish
and subscribe
to it - first, let's add a subscription.
const ably = new Ably.Realtime.Promise({
key: "your-api-key-here",
clientId: "someid",
});
channel = ably.channels.get("your-channel-name");
channel.subscribe((message) => {
messages = [...messages, message];
});
In this block of code, we're subscribing to all the messages that will be sent to the channel, and we're going to add them to our messages
array.
Our messages
array is reactive by default in Svelte
- this means that when it is assigned to in our onMount
callback, it will automatically update the DOM and re-render the app. In the callback passed to the subscribe
call, we're creating a new array with the contents of the existing messages array, and then adding the new message to the end of the array, effectively appending the new message to the end of our collection.
If we stopped here, we would have a terrible bug in our code that would cause our app to use up it's quota of messages quickly - because each time the app mounts, it's subscribing to our channel. This means every time a message arrives, we're adding an additional subscription, and we'll be adding subscriptions exponentially with each new message.
We're going to fix this bug by adding code to the returned function that acts as an onUnmount
hook.
onMount(() => {
....
return () => {
channel.unsubscribe(channel);
channel.detach();
};
});
In our returned function, we're unsubscribing from our channel, and detaching the channel from Ably
. We're now not leaking connections and using up our quotes unnecessarily. Our application will now correctly render any messages that arrive on our channel using the Svelte
HTML loop from our first sample:
<script>
...
</script>
<div class="App">
<h1>Ably Svelte Demo</h1>
<div>
<button>Send Message</button>
</div>
<h2>Messages</h2>
<ul>
{#each messages as msg}
<li>{msg.data.text}</li>
{/each}
</ul>
</div>
The #each
block loops around the messages
array, and for each message in the array, it renders a li
element with the message's text. In order to support actually sending some messages, the UI needs to be updated with a clickable button.
<h1>Ably Svelte Demo</h1>
<div>
<button on:click={sendMessage}>Send Message</button>
</div>
...
Svelte
uses the attribute syntax visible here to bind events to the UI. The button calls a function called sendMessage
when it's clicked.
const sendMessage = () => {
channel.publish("test-message", { text: "message text" });
};
When added to our script blockm this function will use the channel
that we created in the onMount
callback to publish a message to the channel. The message type here is set to test-message
and there's a little hardcoded text that matches what the UI code expects.
The completed App.svelte
application looks like this:
<script>
import { onMount } from "svelte";
import * as Ably from "ably";
let messages = [];
let channel = null;
onMount(() => {
const ably = new Ably.Realtime.Promise({
key: "your-api-key-here",
clientId: "someid",
});
channel = ably.channels.get("your-channel-name");
channel.subscribe((message) => {
messages = [...messages, message];
});
return () => {
channel.unsubscribe(channel);
channel.detach();
};
});
const sendMessage = () => {
channel.publish("test-message", { text: "message text" });
};
</script>
<div class="App">
<h1>Ably Svelte Demo</h1>
<div>
<button on:click={sendMessage}>Send Message</button>
</div>
<h2>Messages</h2>
<ul>
{#each messages as msg}
<li>{msg.data.text}</li>
{/each}
</ul>
</div>
And it works!
It's quite simple for our apps to use the channels from the Ably JavaScript SDK in Svelte
- just so long as we make sure we clean up after ourselves.
In the next post of this series, we'll expand on this sample and add support for Channel Presence
and make use of some Svelte Stores
.
In part 1 of our series on using Ably with Svelte
, we built a small app to publish and subscribe to messages on a channel. In this post, we're going to expand on that app and add support for Channel Presence
.
Ably’s presence feature allows clients or devices to announce their presence on a channel.
Other devices or services may then subscribe to these presence events (such as entering,
updating their state, or leaving the channel) in real time using our realtime SDKs, or
via the Ably Integrations.
Presence is one of Ablys core features - it allows us to keep track of the number of people on a channel, and to know who is online and who isn't.
In addition to the presence events, we can attach a Presence Member Data to each of the clients - this member data can be used to instantly synchronise data between all clients on the same channel - and is great for things like "user is typing" indicators.
We're going to expand the code sample from part 1 in this series, adding a button that can be clicked to send some Presence Member Data.
In part 1, we finished up with this single-component Svelte
application:
<script>
import { onMount } from "svelte";
import * as Ably from "ably";
let messages = [];
let channel = null;
onMount(() => {
const ably = new Ably.Realtime.Promise({
key: "your-api-key-here",
clientId: "someid",
});
channel = ably.channels.get("your-channel-name");
channel.subscribe((message) => {
messages = [...messages, message];
});
return () => {
channel.unsubscribe(channel);
channel.detach();
};
});
const sendMessage = () => {
channel.publish("test-message", { text: "message text" });
};
</script>
<div class="App">
<h1>Ably Svelte Demo</h1>
<div>
<button on:click={sendMessage}>Send Message</button>
</div>
<h2>Messages</h2>
<ul>
{#each messages as msg}
<li>{msg.data.text}</li>
{/each}
</ul>
</div>
We're going to need to add some additional capabilities to make presence work. These are:
- A new variable to store the presence data
- Presence subscription and unsubscription code during
onMount
- HTML elements to display presence data
- A HTML button to send presence data
The code needs modifying to add a variable called presenceData
to store the presence data.
As the start of the script block, this extra delcaration is required:
import { onMount } from "svelte";
import * as Ably from "ably";
let messages = [];
let presenceData = []; // New variable to store the presence data
let channel = null;
onMount(() => {
...
Because variables in Svelte components
are reactive
by default (see Reactive Variables), we can trigger a re-render of the page by changing the value of the variable.
The onMount
code needs to be extended to subscribe to the presence events using the Ably JavaScript SDK. Before the return statement in the onMount
callback, the follow changes are required:
onMount(() => {
... // Omitted for brevity
const updatePresence = async () => {
presenceData = await channel.presence.get();
};
(async () => {
channel.presence.subscribe("enter", updatePresence);
channel.presence.subscribe("leave", updatePresence);
channel.presence.subscribe("update", updatePresence);
await channel.presence.enter("");
presenceData = await channel.presence.get();
})();
return () => {
... // Omitted for brevity
};
});
This is quite a complicated block of code, but there's a method in the madness. onMount
handlers in Svelte
must return a function that can execute synchronously
. Because the code requires the await
keyword when the channel presence is entered, the code needs to be wrapped in a self-executing async
function.
It's absolutely fine in this context, because we can tolerate presence being subscribed to asynchronously.
First, an updatePresence
function is declared that assigns the presenceData
variable whenever a presence event occurs. It may look like fetching the data from Ably
in the code here - but presence data is already cached in the client due to the event triggering.
Next, we subscribe to each of the main presence events - enter
, leave
, and update
and after the channel is entered, and initial presence data assigned.
These changes take care of subscribing to the presence events, but further changes need to be made to the onUnmount
callback to unsubscribe from the presence events, so that the code doesn't repeatedly subscribe when new messages arrive and inadvertently use up message quotas.
return () => {
channel.presence.leave();
channel.presence.unsubscribe("enter");
channel.presence.unsubscribe("leave");
channel.presence.unsubscribe("update");
presenceData = [];
channel.unsubscribe(channel);
channel.detach();
};
Here you can see the channel.presence
leave function is called to leave the channel on unmount, along with unsubscribing
from the same three presence events that were subscribed to. Finally, the presenceData
variable is reinitilised to an empty array.
With all these changes, the onMount
callback now looks like this:
onMount(() => {
const ably = new Ably.Realtime.Promise({
key: "your-api-key-here",
clientId: "someid",
});
channel = ably.channels.get("your-channel-name");
channel.subscribe((message) => {
messages = [...messages, message];
});
const updatePresence = async () => {
presenceData = await channel.presence.get();
};
(async () => {
channel.presence.subscribe("enter", updatePresence);
channel.presence.subscribe("leave", updatePresence);
channel.presence.subscribe("update", updatePresence);
await channel.presence.enter("");
presenceData = await channel.presence.get();
})();
return () => {
channel.presence.leave();
channel.presence.unsubscribe("enter");
channel.presence.unsubscribe("leave");
channel.presence.unsubscribe("update");
presenceData = [];
channel.unsubscribe(channel);
channel.detach();
};
});
We need to add two new UI elements to our App.svelte
- a new button to update the presence data, and a new list to display the presence data.
Here, we can see a new button, with an on:click
handler that's calling a function called update
- we'll create this later.
<div class="App">
<h1>Ably Svelte Demo</h1>
<div>
<button on:click={sendMessage}>Send Message</button>
<button on:click={() => update("hello")}>Update status to hello</button>
</div>
<h2>Messages</h2>
...
Next we'll add a second #each block to loop over our presence data - it's exactly the same as our previous one, we're just reading msg.data
from the presence entry.
...
<h2>Messages</h2>
<ul>
{#each messages as msg}
<li>{msg.data.text}</li>
{/each}
</ul>
<h2>Present Clients</h2>
<ul>
{#each presenceData as msg}
<li>{msg.clientId}: {msg.data}</li>
{/each}
</ul>
</div>
To finish up the work to connect presence data, the update
function referenced in the on:click
handler needs to be added. Here it's added below the existing sendMessage
function.
const sendMessage = () => {
channel.publish("test-message", { text: "message text" });
};
const update = (text) => {
channel.presence.update(text);
};
All the update
function does is call channel.presence.update
, using the same channel
instance that was created in the onMount
callback.
Presence data can be as simple as a text string, or can contain encoded json
data - if the implementation is adjusted to parse the data when it's received.
The final App.svelte
file now looks like this:
<script>
import { onMount } from "svelte";
import * as Ably from "ably";
let messages = [];
let presenceData = [];
let channel = null;
onMount(() => {
const ably = new Ably.Realtime.Promise({
key: "your-api-key-here",
clientId: "someid",
});
channel = ably.channels.get("your-channel-name");
channel.subscribe((message) => {
messages = [...messages, message];
});
const updatePresence = async () => {
presenceData = await channel.presence.get();
};
(async () => {
channel.presence.subscribe("enter", updatePresence);
channel.presence.subscribe("leave", updatePresence);
channel.presence.subscribe("update", updatePresence);
await channel.presence.enter("");
presenceData = await channel.presence.get();
})();
return () => {
channel.presence.leave();
channel.presence.unsubscribe("enter");
channel.presence.unsubscribe("leave");
channel.presence.unsubscribe("update");
presenceData = [];
channel.unsubscribe(channel);
channel.detach();
};
});
const sendMessage = () => {
channel.publish("test-message", { text: "message text" });
};
const update = (text) => {
channel.presence.update(text);
};
</script>
<div class="App">
<h1>Ably Svelte Demo</h1>
<div>
<button on:click={sendMessage}>Send Message</button>
<button on:click={() => update("hello")}>Update status to hello</button>
</div>
<h2>Messages</h2>
<ul>
{#each messages as msg}
<li>{msg.data.text}</li>
{/each}
</ul>
<h2>Present Clients</h2>
<ul>
{#each presenceData as msg}
<li>{msg.clientId}: {msg.data}</li>
{/each}
</ul>
</div>
There's a lot of functionality in ~75 lines of code here - but if this were a real application, there would likely be more involved processing of message data happening. At this point, the amount of code related to connecting to, and managing Ably
connections and presence data dwarfs the rest of the application code.
In part 3, we'll introduce a new npm
package of specially build Ably Lifecycle Functions
to remove some of the complexity from this implementation, and make our application more maintainable again.
In parts 1 and 2 of this series, we took a look at Svelte
, and how to use the Ably JavaScript SDK alongside Svelte
to implement realtime capabilities into a basic web app - it's recommended you read part 1, and part 2, otherwise the rest of this piece won't make much sense.
At the end of part 2, we were left with around ~75 lines of code, and a basic web app that uses Ably - but the application was rapidly approaching the point where almost all of it's code was related to managing an Ably
connection, and this likely would be cause for concern in a real application.
There was also quite a lot of things for the programmer to remember to make sure that we used both Svelte
and Ably
correctly.
- We need to handle component mounting and unmounting
- We need to make sure our Ably code doesn't run on the server, where it makes no sense
- We need to make sure we correctly handle subscriptions and channel detachments, so we don't leak resources and use up our messaging limits
That's a lot of subtle things that a developer could get wrong - so instead of just writing the code to handle these things, we'll introduce a new npm
package of specially build Ably Lifecycle Functions
to remove some of the complexity from this implementation, and make our application more maintainable again. Better to solve the problem once.
We've done this before with our @ably/react-hooks
package, and it solved a whole category of errors for implementors.
We're pleased to announce the release of the Ably Lifecycle Functions package, which provides a set of functions that can be used to manage Ably connections and presence data in Svelte applications.
They're available on npm
today, and their usage looks like this, in an App.svelte
file:
<script>
import { useChannel } from "@ably-labs/svelte-lifecycle-functions";
let messages = [];
const [channel] = useChannel("your-channel-name", (message) => {
messages = [...messages, message];
});
</script>
<h2>Messages</h2>
<ul>
{#each messages as msg}
<li>{msg.data.text}</li>
{/each}
</ul>
Using this package you can:
- Interact with Ably channels using a single function call.
- Send messages via Ably using the channel instances returned from that function
- Get notifications of user presence on channels
- Send presence updates
This package provide a simplified syntax for interacting with Ably, and manage the lifecycle of the Ably SDK instances for you taking care to subscribe and unsubscribe to channels and events when your componenets re-render.
The functions ship as an ES6 module, so you can use the import
syntax in your code.
npm install --save @ably-labs/svelte-lifecycle-functions
This works out of the box in the default Svelte starter templates from the REPL - and you can use the package immediately.
It is strongly recommended that you use Token Authentication, this will require server side code that is outside of the scope of this readme. In the examples below we use an API key directly in the markup, this is for *local development only and should not be used for production code and should not be committed to your repositories.
If you are using SvelteKit
, you should keep your API key on the server side
and create an Endpoint to provide the Token Authentication
values as a prop to your client side application.
Once you've added the package using npm
to your project, you can use the functions in your code.
First, we need to modify our Svelte main.js
or main.ts
file to configure the Ably SDK.
import App from './App.svelte';
import { configureAbly } from "../lifecycle-functions";
configureAbly({ key: "your-api-key-here", clientId: "someid" });
var app = new App({
target: document.body
});
export default app;
configureAbly
matches the method signature of the Ably SDK - and requires either a string
or an AblyClientOptions
. You can use this configuration object to setup your API keys, or tokenAuthentication as you normally would. If you want to use the usePresence
function, you'll need to explicitly provide a clientId
.
You can do this anywhere in your code before the rest of the library is used.
The useChannel function lets you subscribe to a channel and receive messages from it.
const [channel, ably] = useChannel("your-channel-name", (message) => {
console.log(message);
});
Both the channel instance, and the Ably JavaScript SDK instance are returned from the useChannel call.
useChannel
really shines when combined with a regular reactive assignment or store hook - for example, you could keep a list of messages in your app state, and use the useChannel
hook to subscribe to a channel, updating the UI by assigning to the reactive property in your code when new messages arrive.
The usePresence hook lets you subscribe to presence events on a channel - this will allow you to get notified when a user joins or leaves the channel. You need to provide usePresence
with a callback function to accept presence changes.
let presenceData = [];
const [update] = usePresence("your-channel-name", (data) => {
presenceData = data;
});
You can optionally provide a string when you usePresence
to set an initial presence data
string.
const [update] = usePresence("your-channel-name", (data) => { ... }, "initial state");
The update
function can be used to update the presence data for the current client:
updateStatus("new status");
Using this package, we can convert our original source code from looking like this:
<script>
import { onMount } from "svelte";
import * as Ably from "ably";
let messages = [];
let presenceData = [];
let channel = null;
onMount(() => {
const ably = new Ably.Realtime.Promise({
key: "your-api-key-here",
clientId: "someid",
});
channel = ably.channels.get("your-channel-name");
channel.subscribe((message) => {
messages = [...messages, message];
});
const updatePresence = async () => {
presenceData = await channel.presence.get();
};
(async () => {
channel.presence.subscribe("enter", updatePresence);
channel.presence.subscribe("leave", updatePresence);
channel.presence.subscribe("update", updatePresence);
await channel.presence.enter("");
presenceData = await channel.presence.get();
})();
return () => {
channel.presence.leave();
channel.presence.unsubscribe("enter");
channel.presence.unsubscribe("leave");
channel.presence.unsubscribe("update");
presenceData = [];
channel.unsubscribe(channel);
channel.detach();
};
});
const sendMessage = () => {
channel.publish("test-message", { text: "message text" });
};
const update = (text) => {
channel.presence.update(text);
};
</script>
<div class="App">
<h1>Ably Svelte Demo</h1>
<div>
<button on:click={sendMessage}>Send Message</button>
<button on:click={() => update("hello")}>Update status to hello</button>
</div>
<h2>Messages</h2>
<ul>
{#each messages as msg}
<li>{msg.data.text}</li>
{/each}
</ul>
<h2>Present Clients</h2>
<ul>
{#each presenceData as msg}
<li>{msg.clientId}: {msg.data}</li>
{/each}
</ul>
</div>
To looking like this:
<script>
import { useChannel, usePresence } from "@ably-labs/svelte-lifecycle-functions";
let messages = [];
let presenceData = [];
const [channel] = useChannel("your-channel-name", (message) => {
messages = [...messages, message];
});
const [update] = usePresence("your-channel-name", (data) => {
presenceData = data;
});
const sendMessage = () => {
channel.publish("test-message", { text: "message text" });
};
</script>
<div class="App">
<h1>Ably Svelte Demo</h1>
<div>
<button on:click={sendMessage}>Send Message</button>
<button on:click={() => update("hello")}>Update status to hello</button>
</div>
<h2>Messages</h2>
<ul>
{#each messages as msg}
<li>{msg.data.text}</li>
{/each}
</ul>
<h2>Present Clients</h2>
<ul>
{#each presenceData as msg}
<li>{msg.clientId}: {msg.data}</li>
{/each}
</ul>
</div>
Removing all of the glue code concerned with configuring the Ably JavaScript SDK, and keeping the intent of your application front and centre.
We hope that this is a useful addition to your application, and we'd love to hear your feedback on it.
TBD