Brian Mwangi

Back to articles

Enabling real time functionality using Server Sent Events

-- 2 min read

An image of an iphone next to a laptop, a camera lens, and a starbucks cup

What are Server Sent Events?

Server sent events (SSE) are messages that are sent from a web server to a client as a stream. These messages are sent in form of events and they can contain data. These messages are unidirectional, i.e, only from the server to the client; the client cannot send an event to the server.
Server sent events can be used in place of web sockets. They are simpler, but not less capable than web sockets.

Demo Time

Let us see how we can use server sent events to enable real time functionality in a chat application. When a user writes a comment and sends it, another user who is reading the same article will see the comment appear in real time.

In the application above the user adds a comment by submitting the comment in a form and storing it in the database. Pretty standard routine.

// /posts/:slug

import { Form } from "@remix-run/react";
// import createComment

export async function action({ request }) {
  
  // Save comment to the database
  
  let { status } = await createComment(request, comment, userId, postId);
  return { ok: true };
}

export default function Post() {
  return (
    
    // A form to submit the comment
    
  	<Form method="post">
        <label htmlFor="comment">Comment</label
    	<textarea 
    		name="comment"
            id="comment"
    	/>
    	<button type="submit">Submit</button>
    </Form>
  );
} 

How do I achieve the real time functionality?

To use server sent events we need two things:

  • An EventSource to listen to the events from the server. An EventSource opens a connection to the server, which sends events in the text/event-stream format.
  • An EventEmitter to emit events.

In this application I am using Remix framework to build the application. Let us start by building the EventSource.

Remix utils has a nice wrapper for the EvenSource interface. Let's use that.

In the route that renders the blog post and comments, let us create an EventSource instance.

// posts/:slug

import { useEventSource } from "remix-utils/sse/react";

export default function Post() {

  let newMessage = useEventSource('/sse', { event: 'new-message' });

  return (
  <main>
  // ---Blog article---
  </main>
  );
}

We pass a url of a route that sends the streams of events to the useEventSource() hook and an optional event name to listen to.

Let us create the route /sse that sends the event streams:

// /sse

import { eventStream } from "remix-utils/sse/server";
import { emitter } from "~/services/emitter";

export async function loader({ request }) {
    return eventStream(request.signal, function setup(send) {
        function handle(message) {
            send({ event: 'new-message', data: message });
        }
        emitter.on("message", handle);

        return function clear() {
            emitter.off("message", handle);
        };
    });
}
// app/services/emitter

import { EventEmitter } from "node:events";

export let emitter = new EventEmitter();

The code above is just returning a stream of events from the server in the form text/event-stream. We are returning events of the type 'new-message'. The send() function sends data to the client. Now that we can stream events from the server, let us emit an event when one comments on an article.

We do that right after storing the comment in the database. All we need is literally one line of code to emit the event.

// /posts/:slug

import { emitter } from "~/services/emitter";

// import createComment

export async function action({ request }) {
  
  // Save comment to the database
  
  let { status } = await createComment(request, comment, userId, postId);

  // Emit the event after comment is saved in db
  
  emitter.emit("message", comment);
  
  return { ok: true };
}

The last piece of the puzzle is to make the UI refresh after receiving new data from the server. To implement this in Remix you simply use the useRevalidator hook.

// /post/:slug

import { useRevalidator } from "@remix-run/react";
import { useEventSource } from "remix-utils/sse/react";
import { useEffect } from "react";

export default function Post() {
  
  let newMessage = useEventSource('/sse', { event: 'new-message' });
  
  let { revalidate } = useRevalidator();
  
     useEffect(() => {
        revalidate();
    }, [newMessage]);

  return (
    <main>
    // ---Article---
    </main>
  )
}

This makes Remix re-render with the new data from the server which includes the new comments from the database.

Now when one comments on an article, an event will be sent to the client and other people that are reading the same article will see the comment appear in real time.

That's it!!👋🏾. Go implement it in your projects.

WhatsApp icon