Building A Chat Application Using SvelteJS and SSE

Julien Demangeon
Julien DemangeonOctober 02, 2020
#js#tutorial

If you've already developed web applications with ReactJS or VueJS, you've probably heard of SvelteJS.

In this article, I will explore this new framework to discover its subtleties, and show you how it is not so different from others (on the surface).

I'll take advantage of this experience to use the SSE (Server Sent Event) protocol for data exchanges in the same time. It has been confidential for a long time but it is more and more used.

Once Upon a Time, JS Applications Ruled The World

For a long time, web applications were rendered server-side and page changes were based on hyperlink transitions.

In the mid 2000s, pages were visually "dynamized" with the advent of AJAX, and the release of several Javascript utility libraries such as Mootools, script.aculo.us, and the most famous of them, jQuery.

Many other professional libraries have followed them, allowing this time to create complete Javascript applications (aka SPA). These include AngularJS (aka Angular 1), EmberJS and my favorite at that time, BackboneJS.

In the mid-2010s, ReactJS and VueJS changed the game by introducing the concept of "Virtual DOM" and thus reducing the number of mutations on the DOM, which are very slow.

With the evolution of compilers, SvelteJS arrived on the scene of the Javascript world with a bang! This has changed the development habits by bringing back the direct DOM mutation by means of a pre-optimization during the compilation phase.

Parallel to the arrival of SvelteJS, a second wave (also partially based on pre-compilation) that stimulates my curiosity is that of StencilJS and WebComponents. As such, I also invite you to read my article on the subject.

Today, with the evolution of IOT and the growing need for fast, lightweight and executable applications on small terminals, performance is more than ever at the centre of concerns. That's why it is essential to adapt our tools.

SvelteJS, A New Kid In Town

SvelteJS was developed by Rich Harris, who is also the creator of the Javascript compiler / bundler called RollupJS. The first version of the library was released at the end of 2016.

It has been developed to relieve the browser of the unnecessary and expensive processing introduced by the Virtual DOM diffing of recent frameworks.

Thus, SvelteJS has moved this complexity to the compilation stage, partialy reducing the weight of Javascript bundles and speeding up rendering times.

Being fully compiled, SvelteJS is not dependent on any existing language, allowing it to introduce its own syntax and templating language.

Although flexible in its syntax and functionality, SvelteJS strives to adhere as much as possible to component development conventions such as props, contexts, lifecycle events, etc.

Server Sent Events, Call Me Back

Not so long ago, Alexis and I used Firebase to create an application. We were quickly impressed by the instantaneous feedback from the server.

Always driven by the desire to understand how things work, I analyzed the operation in the Chrome devtools, expecting to discover some kind of long server polling. I realized that Firestore is in fact using Server-Sent Events (SSE).

SSE (Server Sent Events) designates events (data) that are sent from the server to the browser as a continuous stream. This is why it is also sometimes called "Event Stream".

Unlike Websockets, which use a continuous TCP connection, SSE use the HTTP layer. In addition, while a Websocket is bi-directional and therefore allows data exchange in both directions, SSE only allow data to be sent back from the server in one direction. To send messages to the server, it is necessary to expose another HTTP endpoint.

There are so many advantages (also ecological!) to list that it could be the subject of a whole article. By the way, on this subject I invite you to read An article about server-sent events on API Friends, which is very well written and describes the differences between SSE, Long-polling and Websockets.

An evidence of the growing attractiveness of SSE, new protocols such as Mercure use SSE to add higher-level functionality to the web stack.

Use Case: A Chat Application

As for all my hackday projects, I usually try to create an application as close as possible to real world scenarios in order to dig as much as possible into my subject.

In this exploration, I developed a chat application because it requires both good performance on the frontend (light Javascript files, fast loading times) and a good reactivity through SSE (instant feedbacks).

Below, I will describe the chosen architecture, then I will get to the heart of the matter through code examples.

Chat architecture

As with most modern applications, the chat consists of both a frontend part (developed in SvelteJS) and a backend part (developed in NodeJS for convenience).

For ease of use, these two parts have been embedded in the same docker-compose.yml file.

version: '3.3'

services:
    frontend:
        image: node:13-stretch
        working_dir: '/app/packages/frontend'
        command: yarn dev
        user: '${UID}:${GID}'
        ports:
            - 5000:5000
            - 35729:35729
        volumes:
            - .:/app

    backend:
        image: node:13-stretch
        working_dir: '/app/packages/backend'
        user: '${UID}:${GID}'
        command: yarn dev
        ports:
            - 3000:3000
        volumes:
            - .:/app

The frontend chat application consists of 2 separate pages. The homepage exposes a form field to enter the name of a channel to access and the username to use. The second route contains the actual chat.

The chat backend is made up of an ExpressJS server that exposes two endpoints:

The "POST" endpoint "/{channelId}/post" allows you to send messages in a channel.

The "LISTEN" endpoint "/{channelId}/listen" exposes an HTTP stream (actually an SSE stream) of all posted messages.

So, each message sent to the "POST" endpoint is broadcast on all the listening "LISTEN" streams.

I used Memory as storage system for the messages, but many other storage systems could also be used. One could even imagine using an asynchronous queue such as RabbitMQ, Apache Kafka, Amazon SQS, etc... to produce or consume messages from any storage source. This is one of the main advantages of event-driven architectures.

The Node.js Backend

As already explained, the backend server consists of 2 basic endpoints powered by ExpressJS. It's hard to make a simpler setup.

{
    "name": "backend",
    "version": "1.0.0",
    "private": true,
    "license": "MIT",
    "scripts": {
        "dev": "nodemon --watch ./src ./src/server.js"
    },
    "dependencies": {
        "express": "4.17.1"
    },
    "devDependencies": {
        "nodemon": "2.0.3"
    }
}

Here is the first endpoint of "POST" which receives, records then broadcasts the messages to the clients connected to the "LISTEN" endpoint (that I will describe afterwards).

const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');

const app = express();

app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

app.post('/:channelId/send', (req, res, next) => {
    const { channelId } = req.params;

    // Store message, then broadcast messages here...

    return res.send('ok');
});

app.listen(3000, function() {
    console.log('SSE Tchat listening on port 3000!');
});

And now the "LISTEN" part.

const express = require("express");
const bodyParser = require("body-parser");
const cors = require("cors");

const app = express();

+ const channels = {};

+ function sendEventsToAll(event, channelId) {
+   if (!channels[channelId]) {
+     channels[channelId] = [];
+   }
+
+   channels[channelId].forEach((c) =>
+     c.res.write(`data: ${JSON.stringifY(event)} \n\n`)
+   );
+ }

app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

app.post("/:channelId/send", (req, res, next) => {
  const { channelId } = req.params;

-  // Broadcast messages here...
+  sendEventsToAll(req.body, channelId);

  return res.send("ok");
});

+ app.get("/:channelId/listen", function (req, res) {
+  res.writeHead(200, {
+    "Content-Type": "text/event-stream",
+    Connection: "keep-alive",
+    "Cache-Control": "no-cache",
+  });

+  const { channelId } = req.params;
+  const clientId = Date.now();

+  if (!channels[channelId]) {
+    channels[channelId] = [];
+  }

+  channels[channelId].push({
+    id: clientId,
+    res,
+  });

+  const data = `data: ${JSON.stringify([
+    {
+      username: "Bot",
+      message: "Welcome! Happy to see you ;)",
+      time: Date.now(),
+    },
+  ])}\n\n`;

+  res.write(data);

+  req.on("close", () => {
+    console.log(`${clientId} Connection closed`);
+    channels[channelId] = channels[channelId].filter((c) => c.id !== clientId);
+  });
+ });

app.listen(3000, function () {
  console.log("SSE Tchat listening on port 3000!");
});

Well, it's hard to believe, but our backend is ready. At least for a proof-of-concept. In a real world application, I would need to implement a heartbeat system, to account for network devices who cut inactive HTTP connections. I would also need to store connections in a shared database instead of in memory, like a Redis server, to allow the connections to scale beyoud what a single server can handle.

The Svelte Frontend

Now that the backend is ready, I'm going to initialize the frontend application.

For this, I will use the degit command directly from npx. This command allows to initialize a project directly from a github repository (here a Svelte application template).

Fun fact, degit was also developed by Rich Harris, the creator of SvelteJS!

npx degit sveltejs/template packages/frontend

And here is the generated file structure.

.
├── package.json
├── public
│   ├── favicon.png
│   ├── global.css
│   └── index.html
├── README.md
├── rollup.config.js
└── src
    ├── App.svelte
    └── main.js

2 directories, 8 files

Anatomy Of A Svelte Component

With SvelteJS, you just have to create a component and then call its constructor with the right mount point (here "target" document.body) to attach it to the DOM.

In my case, it's the original App.svelte file that I've modified to put a router in it.

// ./src/App.svelte

<script>
    import Router from 'svelte-spa-router';
    import Home from './routes/Home.svelte';
    import Channel from './routes/Channel.svelte';

    const routes = {
        '/': Home,
        '/channel/:id': Channel,
    };
</script>

<style>
    .root {
        padding: 10px;
    }
</style>

<div class="root">
    <Router {routes} />
</div>
// ./src/main.js

import App from './App.svelte';

const app = new App({
    target: document.body,
    props: {},
});

export default app;

As you can see, the structure of a SvelteJS component is very close to the structure of VueJS .vue files.

Thus, it is possible to integrate javascript (between <script> tags), css (between <style> tags) and some kind of html at the same level.

Svelte Templating Syntax and Bindings

The Svelte syntax is so rich that it would be impossible to detail everything in this article. Nevertheless, here is an overview of what I had the opportunity to use in this project.

As with most component libraries, Svelte uses a declarative tag structure to design applications. As in JSX, tags written in lowercase refer to native DOM elements (div, span, ...), while tags with uppercase names refer to non-native components. These need to be imported in the <script> tag to be used.

// ./src/routes/Channel.svelte

<script>
    import Chat from '../components/Chat.svelte';
    export let params = {};
</script>

<style>
    /* ... */
</style>

<div class="root">
    <Chat channelId={params.id} />
</div>

Variables declared in the <script> section can be scoped localy or modified from the outside (just like component props) if they are preceded by the export keyword.

// ./src/components/ChatHeader.svelte -->

<script>
    // This "exported" variable can be changed (it's a prop!)
    export let title = 'Chat'; // "Chat" is the default prop value

    // This one is scoped to the component (it can't be changed from outside)
    let messageCount = 0;
</script>

<style>
    /* ... */
</style>

<div class="root clearfix">
    <div class="about">
        <div class="with">{title}</div>
        <div class="num-messages">
            already {messageCount > 1 ? `${messageCount} messages` :
            `${messageCount} message`}
        </div>
    </div>
</div>

Every expression between {} will be evaluated Javascript. We can also use these expressions as attributes or even use them as shorthand if the name is the same as the tag attribute.

<script>
    let src = './alittlecat.png';
    let className = 'myimage';
</script>

<style>
    /* ... */
</style>

<img class={className} {src} />

Just like Angular and VueJS, SvelteJS uses a specific binding markup to trigger actions on DOM Events. Here's an example with the ChatInput.

// ./src/components/ChatInput.svelte

<script>
    let value = '';

    const handleSubmit = () => {
        // Do something with the value...

        value = ''; // Reset the value
    };
</script>

<style>
    /* ... */
</style>

<div class="root">
    <textarea bind:value placeholder="Type your message" rows="3" />
    <button on:click={handleSubmit}>Send</button>
</div>

The on directive allows to trigger functions when DOM or custom events occur.

In the above example, I use the bind:value shorthand because the variable is called value. In most cases, the binding follows the following syntax: bind:value={myvariable}.

The bind directive is bi-directionnal, that means it is possible to change the value in the textarea from the outside, as in this case from handleSubmit, which resets the value after sending.

It would also have been possible to use an uni-directionnal control flow using on:change and value={value} together.

SvelteJS provides computed properties just like VueJS. Thus, for example, it would have been possible to count the number of remaining characters the following way:

$: remainingCharacters = 100 - value.length;

So, each time value changes, the remainingCharacters value gets computed again.

We can even execute blocks of Javascript based on value changes. For example, we can trigger an alert() when the remainingCharacters count is under 0.

$: {
    if (remainingCharacters < 0) {
        alert(`There's to many characters (${remainingCharacters})!`);
    }
}

Shortly:

$: if (remainingCharacters < 0) {
    alert(`There's to many characters (${remainingCharacters})!`);
}

In order to work, variables must be immutables.

SvelteJS also provides all the basic features of a templating language for conditions ({#if}{:else}{/if}) and loops ({#each}{/each}).

// ./src/components/Message.svelte

<script>
    export let alignRight = false;
</script>

<style>
    /* ... */
</style>

{#if alignRight}
<div>
    <!-- Right aligned message -->
</div>
{:else}
<div>
    <!-- Left aligned message -->
</div>
{/if}
<script>
    import Message from './Message.svelte';
    let messages = [];
</script>

<style>
    /* ... */
</style>

{#each messages as message, i}
<li class="clearfix">
    <Message alignRight={i % 2} {message} />
</li>
{/each}

It is recommended to define a key for each displayed line of the list. Unlike ReactJS, which uses the key property in each listed component, Svelte allows to define this key directly between brackets at the root of the iterator.

{#each messages as message, i (message.uuid)}
    <!-- ... -->
{/each}

Best of all, svelte allows you to integrate asynchronous logic directly into the templates. I didn't have the opportunity to use this feature, but I just couldn't avoid talking about it.

{#await promise}
	<p>...waiting</p>
{:then number}
	<p>The number is {number}</p>
{:catch error}
	<p style="color: red">{error.message}</p>
{/await}

I have covered the main statements. However, many others are available (@debug, @html, ...), here is the related documentation.

Svelte Lifecycle Events

As in most component-based frameworks, Svelte provides a system to perform component lifecycle actions.

So, it is possible to execute instructions on the following hooks: onMount, onBeforeUpdate, onAfterUpdate.

Just like the ReactJS useEffect, the function returned by the onMount callback is called when the component is unmounted. Here's an example from the documentation.

<script>
	import { onMount } from 'svelte';

	onMount(() => {
		const interval = setInterval(() => {
			console.log('beep');
		}, 1000);

		return () => clearInterval(interval);
	});
</script>

In my case, I relied on the beforeUpdate and afterUpdate lifecycle events in order to manage the automatic scroll when a new message arrives.

<script>
  import { onMount, beforeUpdate, afterUpdate } from "svelte";
  import Message from "./Message.svelte";

  let div;
  let autoscroll;

  beforeUpdate(() => {
    autoscroll =
      div && div.offsetHeight + div.scrollTop > div.scrollHeight - 20;
  });

  afterUpdate(() => {
    if (autoscroll) div.scrollTo(0, div.scrollHeight);
  });
</script>

<style>
    /* ... */
</style>

<div class="root">
  <div class="history" bind:this={div}>
    <ul>
      {#each messages as message, i}
        <li class="clearfix">
          <Message alignRight={i % 2} {message} />
        </li>
      {/each}
    </ul>
  </div>
</div>

Svelte Context, Data Stores, and Event Dispatchers

In addition to its rich syntax, Svelte provides a set of features to facilitate data management and data flow between components.

Therefore, as with ReactJS, a context system allows to expose data across the entire component hierarchy.

// ParentComponent.svelte

<script>
    import { setContext } from "svelte";
    setContext("answer", 42);
</script>

<div>
    <ChildComponent />
</div>
// ChildComponent.svelte

<script>
    import { getContext } from "svelte";
    const answer = getContext("answer");
</script>

<div>{answer}</div>

In addition to the context that allows data to flow from the top to the bottom of the hierarchy, SvelteJS provides an Event Dispatcher system that allows sending events from child components. This is a more generic equivalent to the onDoSomething functions that are passed to child components in ReactJS.

This is the way I've chosen to forward messages from the ChatInput.svelte to the Chat.svelte component which controls the whole chat logic.

// ./src/components/Chat.svelte (<script> part)

const handleSendMessage = async e => {
    await fetch(`http://localhost:3000/${channelId}/send`, {
        body: JSON.stringify({
            message: e.detail.text,
            username,
            time: Date.now(),
        }),
        headers: {
            'Content-Type': 'application/json',
        },
        method: 'POST',
    });
};
// ./src/components/Chat.svelte (component part)

<ChatInput on:message={handleSendMessage} />
// ./src/components/ChatInput.svelte (<script> part)

import { createEventDispatcher } from 'svelte';

const dispatch = createEventDispatcher();
let value = '';

const handleSubmit = () => {
    dispatch('message', { text: value });

    value = '';
};
// ./src/components/ChatInput.svelte (component part)

<div>
    <textarea bind:value placeholder="Type your message" rows="3" />
    <button on:click={handleSubmit}>Send</button>
</div>

As you see here, we listen to the on:message because we dispatch an event using the message event name. It is thus possible to dispatch any event.

Last but not least, Svelte offers a store system that can be divided into 3 distinct types.

  • The writable type, which is readable and writable from the outside.
  • The readable type, which is only accessible in read mode
  • The derivable type, which is only readable and whose value is calculated from other stores values.

As with RXJS observables, objects returned by stores implement the subscribe method, which allows to add a listener on value changes. They also implement an unsubscribe method to unsubscribe to events. The writable store object also exposes a set and update method to mutate inner value.

In my case, I used a writable store to forward messages from the SSE EventSource stream to the chat thread.

// ./src/channel/store.js

import { writable } from 'svelte/store';

export const createChannelStore = channelId => {
    const { subscribe, set, update } = writable([]);

    const eventSource = new EventSource(
        `http://localhost:3000/${channelId}/listen`,
    );

    eventSource.onmessage = e => {
        update(messages => messages.concat(JSON.parse(e.data)));
    };

    return {
        subscribe,
        reset: () => set([]),
        close: eventSource.close,
    };
};
// ./src/components/Chat.svelte

import { onMount } from 'svelte';
import { createChannelStore } from '../channel/store';

export let channelId;
let messages = [];

onMount(() => {
    const store = createChannelStore(channelId);

    store.subscribe(incomingMessages => {
        messages = incomingMessages;
    });

    return store.close;
});

Svelte Children and Slots

In most cases, a component can have children. In ReactJS it is a matter of integrating the value of the prop children where we want in the child component as for any other value. With SvelteJS the slot tag is used to achieve the same result.

// Main.svelte

<Parent>
    <p>Children content...</p>
</Parent>
// Parent.Svelte

<div>
    <slot>
        This is the default content... It'll be replaced by "Children
        content..." from the Main.svelte file
    </slot>
</div>

Just like with StencilJS, SvelteJS allows to transform a component tree into a real template! Indeed, thanks to the named slots, it is possible to integrate components at different places in a declarative way.

// Main.svelte

<Parent>
    <p slot="first">First children content...</p>
    <p slot="second">Second children content...</p>
</Parent>
// Parent.Svelte

<div>
    <slot name="first">
        This will be replaced by "First children content..."
    </slot>
    <span>
        <slot name="second" />
    </span>
</div>

Conclusion

This exploration aside of the major JS frameworks was very refreshing. Not only did it allow me to see that it was possible to create the same type of application rich in functionality, but also with optimal performance.

Svelte is really promising, but its ecosystem is lacking when compared to the major JS frameworks. For instance, I couldn't find a good UI or form library. That means that choosing SvelteJS today implies writing a lot more code tahn choosing React, Angular, or Vue - because I'd have to reimplement too many things.

Regarding the use of Server-Sent Events, I was surprised by the simplicity of a basic implementation on the client and server side. But persistent connections is a harder problem than it seems, and I know there is a lot more to do to build a robust SSE server.

To conclude, between my exploration of ReasonML and this one, I struggle to understand why these technologies do not emerge more in the Javascript ecosystem. Nevertheless, I hope to contribute in my own way with this article.

The final result is available on GitHub: svelte-sse-chat. Feel free to comment and improve!

Did you like this article? Share it!