DEV Community

Cover image for Building with server-sent events with React and Node.js
Shubham Naik
Shubham Naik

Posted on

Building with server-sent events with React and Node.js

This post is a mirror of a post I wrote on my own blog. If you would like python or native javascript examples of the code I presented below, feel free to check it out here

Building real-time applications on the web has never been easier. In this post, I'll explain how you can use server-sent events, or SSE for short, to get real-time data for your web applications.

At the end of this article you should know:

  • What a server-sent event is
  • How to listen to server-sent events on the browser
  • How to send server-sent events from your server

This tutorial is for those who have some familiarity with developing on the web as well as some knowledge in either python or nodejs.

A light-hearted illustration of how SSE events work

The gist

Server-sent events (SSE) are a client initiated, unidirectional, server controlled messages. When you visit a website that queries an SSE-enabled endpoint, the server can send your browser unlimited amounts of information until you leave that page. SSE urls are always accessed via an asynchronous request from your browser. You can visit a url that serves an SSE endpoint from your browser but there is no standard on what you will experience.

const source = new EventSource('/an-endpoint');

source.onmessage = function logEvents(event) {
   console.log(JSON.parse(data));
}
Enter fullscreen mode Exit fullscreen mode

In this code snippet, I create a new EventSource object that listens on the url /an-endpoint. EventSource is a helper class that does the heavy lifting of listening to server-sent-events for us. All we need to do now is to attach a function, in this case logEvents, to the onmessage handler.

Any time our server sends us a message, source.onmessage will be fired.

Let's look at a more realistic example. The code below listens on a server at the url https://ds.shub.dev/e/temperatures. Every 5 seconds, the server returns a server-sent event with the temperature of my living room.


// @codepen-link:https://codepen.io/4shub/pen/QWjorRp
import React, { useState, useEffect } from 'react';
import { render } from "react-dom";

const useEventSource = (url) => {
    const [data, updateData] = useState(null);

    useEffect(() => {
        const source = new EventSource(url);

        source.onmessage = function logEvents(event) {      
            updateData(JSON.parse(event.data));     
        }
    }, [])

    return data;
}

function App() {
  const data = useEventSource('https://ds.shub.dev/e/temperatures');
  if (!data) {
    return <div />;
  }

  return <div>The current temperature in my living room is {data.temperature} as of {data.updatedAt}</div>;
}

render(<App />, document.getElementById("root"));
Enter fullscreen mode Exit fullscreen mode

What's happening behind the scenes?

A diagram showing the steps of an event source request

Let's look at these two properties of EventSource:

  • url - The url that we want to listen on for changes
  • readyState - The state of the connection. This can be (0) CONNECTING, (1) OPEN and (2) CLOSED. Initially this value is CONNECTING.

When EventSource is invoked, the browser creates a request with the header Accept: text/event-stream to the url that was passed through.

The browser will then verify if the request returns a 200 OK response and a header containing Content-Type: text/event-stream. If successful, our readyState will be set to OPEN and trigger the method onopen.

The data from that response will then be parsed and an event will be fired that triggers onmessage.

Finally, the server we pinged can send us an unlimited amount of event-stream content until:

  • We close our page
  • We fire the close() method on event source
  • The server sends us an invalid response

When we finally close our connection, the EventSource object's readyState will fire a task that sets readyState to CLOSED and trigger the onclose event.

In case of a network interruption, the browser will try to reconnect until the effort is deemed "futile," as determined by the browser (unfortunately, there are no standards on what constitutes "futile").

Sending events on the server

Sending server-sent events is just as easy as listening to them. Below, I've written a few different implementations of sending server-sent events to your client.

// @repl-it-link:https://repl.it/@4shub/server-sent-events-node
const express = require('express');

const server = express();
const port = 3000;

// create helper middleware so we can reuse server-sent events
const useServerSentEventsMiddleware = (req, res, next) => {
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');

    // only if you want anyone to access this endpoint
    res.setHeader('Access-Control-Allow-Origin', '*');

    res.flushHeaders();

    const sendEventStreamData = (data) => {
        const sseFormattedResponse = `data: ${JSON.stringify(data)}\n\n`;
        res.write(sseFormattedResponse);
    }

    // we are attaching sendEventStreamData to res, so we can use it later
    Object.assign(res, {
        sendEventStreamData
    });

    next();
}

const streamRandomNumbers = (req, res) => {
    // We are sending anyone who connects to /stream-random-numbers
    // a random number that's encapsulated in an object
    let interval = setInterval(function generateAndSendRandomNumber(){
        const data = {
            value: Math.random(),
        };

        res.sendEventStreamData(data);
    }, 1000);

    // close
    res.on('close', () => {
        clearInterval(interval);
        res.end();
    });
}

server.get('/stream-random-numbers', useServerSentEventsMiddleware, 
    streamRandomNumbers)


server.listen(port, () => console.log(`Example app listening at 
    http://localhost:${port}`));
Enter fullscreen mode Exit fullscreen mode

In the example above, I created a server with an event-stream that sends users a random number every second.

Conclusion

Many companies use server-sent events to pipe data to their users in real time. LinkedIn uses server sent events for their messaging service, Mapbox uses SSE to display live map data, and many analytics tools use SSE to show real-time user reports. SSE will only become more prominent as monitoring tools and real-time events become more relevant to users.

Let me know if you try it out โ€” I'd love to see what you come up with!

Top comments (17)

Collapse
 
gandalfarcade profile image
Chris Mumford

Excellent post! This is the first time I've heard of SSE. I don't know how I haven't come across this before. WebSockets are my usual go to for this sort of operation, however, if the client does not need to send any messages back to the server, then it seems SSE is the obvious choice. The automatic reconnection is very cool ๐Ÿ˜.

The only thing that I can see that may be a problem is the browser compatibility of SSE vs WebSockets (but I guess polyfills are there to be used).

Collapse
 
4shub profile image
Shubham Naik

Thank you!

As for support, when building a production application, you should look into this polyfill:
github.com/Yaffle/EventSource

Collapse
 
codechung profile image
CodeChung

Hey Shubham,
Thanks for the article, learned a lot!
I'm having a weird issue where the server is getting hit and emitting responses, but it takes a couple minutes for the client to receive them.
Any idea what could account for this?

Collapse
 
4shub profile image
Shubham Naik • Edited

Couple minutes ? That is interesting, is this your own server or is it the server I set up?

I would say if itโ€™s not my server, it might be due to the size of the data you are sending to the client. If you have a code sample to share, please do

Collapse
 
bryanforst profile image
bryan forst

Ive done a lot of development but this is my first node app using express.

Ive implemented your back end as is in a middleware component on my node server app. However whenever is issue the server.get call my main app.js throws an ERR_HTTP_INVALID_STATUS_CODE error after running through all of the app.use methods that redirect api responses.

I wonder if you have time to look at my code. Im sure Im making a rookie mistake.

Bryan

Collapse
 
4shub profile image
Shubham Naik

Hi Bryan!

Sorry for the lack of response, Iโ€™d love to look at your code, send me a dm of your git repo and Iโ€™ll check it out if youโ€™re still having this issue.

Collapse
 
bryanforst profile image
bryan forst

Its not up yet on git since its not working. also the repo is in another fellows name...
I started to try and solve the server-sent-events issues using express-sse.
That is throwing a 404 error. I will send you the stackoverflow question once i get it entered.

Thread Thread
 
bryanforst profile image
bryan forst

Finally decided to just do a specific implementation outside of my other project. I put it up as public at
github.com/bryanforst/sse-test

The client is still throwing a 404 error - odd thing is that its just showing localhost:3000 and not the endpoint /sseconnect.

I am sure its something stupid I am missing. Thanks for any advice you can give

Thread Thread
 
4shub profile image
Shubham Naik • Edited

I think you might be running two servers that are both fighting for port 3000. There is a server running hosting your react app and a server running hosting your SSE events.

There are two ways we can solve this problem:

  1. Changing one of the ports to a different port.
    This is pretty simple, just change the port of your SSE server to 3001 instead of 3000 and change the url you are connecting to to http://localhost:3001/server-sent-events-url

  2. Hosting the server code and the react code on the same server
    I think this article has some good info on running an express server with react.

dev.to/dan_mcm_/leveling-up-from-c...

I'll also see if I have some time to add a react example to this codebase so people can better try it out end to end with react.

Thread Thread
 
bryanforst profile image
bryan forst

Thanks Shubham,
Originally in my app the back end was listening on 5000 with the front end on 3000. When I attempted to set EventSource to the back end I was getting an error message. That confused me.

Indeed after setting the back end in my test to listen at 5000 all worked well.

Will try to integrate this into my main app.

Thanks for all your help so far
bryan

Thread Thread
 
4shub profile image
Shubham Naik

Perfect! Glad it worked out for you

Collapse
 
wsh4and profile image
Eko Andri Subarnanto

Hi,

  1. How do you do sse if you want to wait for the long running query to be completed?
  2. How do you close connection if you already receive the completed data?

Thank you for the tutorial, I learned a lot.

Collapse
 
4shub profile image
Shubham Naik

Hi there, sorry for late response!

  1. I am not sure, could you rephrase this question?
  2. you should invoke res.end() when you want to close the connection.

Best,
Shubham

Collapse
 
wsh4and profile image
Eko Andri Subarnanto

Hi Shubham, thank you for your reply

  1. Let's say that I want to perform a long running query in the backend, and I want to inform the UI the percentage of how much it is done, say 10%, 20% and so on. Is it through a sse connection or with something else? So the backend keeps sending the 'value' of how much the query has been done? And which part close the connection, UI atau backend?
  2. Thank you.

Regards,
Andri

Thread Thread
 
4shub profile image
Shubham Naik

You could use an SSE connection for this yes, and then you could close the connection when value = 100?

Thread Thread
 
wsh4and profile image
Eko Andri Subarnanto

Oh I see, yes thank you for your reply

Collapse
 
soniar4i profile image
Sonia C.

Hi Shubham, really nice article.

I'm starting to have a look to SSE and I was wondering if there's a way to have something like "channels" to stream only to certain users and to keep those "user sessions" alive to send messages, so far I've seen arrays to keep a user list but that's not very real life approach... any idea on how this would work? Something like redis could be useful? How do I keep the session when sending a message in the future should I have those connections in my server memory?