Real-time Server Sent Events with React & Event Source

Dynamic Website Design

Real-time streaming data is all the rage these days. So in this post, we will create a dynamic website example using real-time web technologies.

This example will use Node.js and Reactjs along with Event Source to create a UI that consumes a data stream. The data will be related to cryptocurrency and CryptoCompare will be the data source. The application auto-updates every 10 seconds usingevent source and the demo is available here:

dynamic website design

Additionally, the source code is available here.

Why Server-Sent Events?

Imagine a scenario, where a web application needs to receive updated data every few seconds. Every time an update has occurred, the UI should change to reflect the new data updates. Essentially, the UI should be reactive!

This application can consider making multiple HTTP requests to retrieve new data every few seconds. However, this isn’t efficient. By opening up new HTTP connections every few seconds we are unnecessarily increasing the load on the server.

Instead, we want to have one HTTP connection kept open. This will provide a stream of data that will be consumed by the client-side in timed intervals. The UI will be reactive to these updates and will re-render every time it receives an update event.

With JavaScript, specifically Nodejs, we will create a stream reader and a stream writer. There are multiple ways of doing this, however, the use of Event Source on the client-side provides the application with a nice API containing multiple options to receive ever-changing data.

Event Source opens a persistent connection to a HTTP Server. It sends server-side events with a connection header that specifies that the request is of type text/event-stream.

Event sourcing is a programming concept that has existed in many Object Oriented languages, such as Java.

Event sourcing is a great way to atomically update state and publish events. The traditional way to persist an entity is to save its current state. Event sourcing uses a radically different, event-centric approach to persistence.Eventuate.com

dynamic website examples

Setting up the client side Stream reader

Lets say we have a React component using TypeScript that contains the following:

  • A dropdown selector that loads a list of cryptocurrency coins on the components mount lifeycycle
  • An Event Source constructed object that initiates a request to our Node.js server
  • An onmessage callback method from the Event Source object that receives the new cryptocurrency data and updates the value shown in the component.
  • Axios to handle all HTTP requests
  • A method getPriceChange() that detects whether the price of the selected coin has increased or decreased
import * as React from 'react';
import { CoinInfo } from './types/CoinInfo';
import './App.css';
import axios from 'axios';
import { ClipLoader } from 'react-spinners';

export type State = {
  loading: boolean;
  coinTypes: CoinInfo[];
  data: any;
  selectedCoin: CoinInfo | undefined;
  selectedCoinPrice: string;
  priceIncrease: boolean;
  selectedCoinSymbol: string
};

export type Props = {};

class App extends React.Component<Props, State> {
  private eventSource: EventSource | undefined;
 
  constructor(props: Props) {
    super(props);
    this.eventSource;
    this.state = {
      data: [],
      selectedCoinSymbol: 'BTC',
      priceIncrease: false,
      selectedCoin: undefined,
      selectedCoinPrice: '',
      coinTypes: [],
      loading: true,
    };
  }

  componentWillMount() {
    this.getCoinTypes();
    this.getCoinCompare();
  }

  componentWillUnmount() {
     if(this.eventSource)
     this.eventSource.close();
  }

  startEventSource(coinType: string) {
    this.eventSource = new EventSource(`http://localhost:5000/coins?coin=${coinType}`);
    this.eventSource.onmessage = e =>
    this.updateCoins(JSON.parse(e.data));
  }

  updateCoins(prices: any) {
   this.getPriceChange(prices.EUR)
   this.setState(Object.assign({}, { selectedCoinPrice: prices.EUR }));
  }

  private async getCoinCompare(coinType?: string) {
   ...get Data! (available in source code)
  }

  private getCoinTypes() {
   ... get Coin Types Data! (available in source code)
  }

   onSymChange(e: React.ChangeEvent<HTMLSelectElement>) {
    this.getCoinCompare(e.target.value);
    this.startEventSource(e.target.value);
  }

  getPriceChange(price: any) {
    const { selectedCoinPrice } = this.state;
    let priceIncreased: boolean = false;
    price > selectedCoinPrice ? priceIncreased = true : priceIncreased = false;
    this.setState({ priceIncrease: priceIncreased })
  }


  public render() {
   // render here
}

export default App;

When the component mounts we are calling two methods. getCoinTypes() which populates the select dropdown with coins. And getCoinCompare() gets the current value of the selected crypto currency.

componentWillMount() {
    this.getCoinTypes();
    this.getCoinCompare();
  }

Our method getCoinCompare()will call the startEventSource() function:

  private async getCoinCompare(coinType?: string) {
    if (coinType) this.setState({ loading: true });

    let coinToCompare = coinType ? coinType : 'BTC';
   
    const res = axios.get(
      `https://min-api.cryptocompare.com/data/pricefsym=${coinToCompare}&tsyms=${
        coinType ? coinType + ',' : ','
      }USD,EUR`
    );

    this.startEventSource(coinToCompare);

    const response = await res;

    let coinPrice = response.data;

   if (coinType)
      this.setState({
        selectedCoinPrice: coinPrice.EUR,
        selectedCoinSymbol: coinType,
        loading: false 
      });
}     

startEventSource() will initiate create Event Source connection.

 startEventSource(coinType: string) {
    this.eventSource = new EventSource(`http://localhost:5000/coins?coin=${coinType}`);
    this.eventSource.onmessage = e =>
    this.updateCoins(JSON.parse(e.data));
  }

The EventSource() constructor creates an object that initializes a communication channel between the client and the server. The created connection is unidirectional, so the events will flow from the server to the client.

However, we also want to send the name of the coin in the request. So we append a query parameter ?coin=${coinType}. Our server will need this to query our external CryptoCompare endpoint for a specific coin.

this.eventSource will establish a connection to our Nodejs server (which we will set up in the next step) as it will be listening on port 5000.

.listen(5000, () => {
    console.log("Server running at http://127.0.0.1:5000/");
  }); 

Setting up our Node Streaming server

This is where the beauty of the Event Source lies. It hits our Nodejs backend endpoint so we can send it back some real-time cryptocurrency data. So, how can we achieve this with our own Node.js file? Well, by using server-sent events this data can be sent back to our client.

Server Sent Events

dynamic website design

 

First, we need to create a server.js file in order for the application to send EventStream data to our React Component on the client-side. Inside this file, we will set up a Nodejs timer.

This isn’t very cumbersome as we don’t need to utilize any external Node packages. We can create this event stream using utilities that are already included with Nodejs. Let start by creating our server:

http
  .createServer((request, response) => {
    console.log("Requested url: " + request.url);
    var url_parts = url.parse(request.url, true);
    var query = url_parts.query.coin
    console.log(query);
    if (request.url.toLowerCase().includes("/coins" )) {
        response.writeHead(200, {
            Connection: "keep-alive",
            "Content-Type": "text/event-stream",
            "Cache-Control": "no-cache",
            "Access-Control-Allow-Origin": "*"
          });
        clearInterval(timer);
        timer = setInterval(() => {
            response.write("\n\n");
            getCoins(query).then(res => {
                response.write(`data: ${JSON.stringify(res)}`);
                response.write("\n\n");
                console.log('check');
                console.log(JSON.stringify(res));
        })
           
          }, 10000);

      response.on('close', () => {
    if (!response.finished) {
      console.log("CLOSED");
      clearInterval(timer);
      response.writeHead(404);
    }
  });

    } else {
      response.writeHead(404);
      response.end();
    }
  })
  .listen(5000, () => {
    console.log("Server running at http://127.0.0.1:5000/");
  });

Let go through this file:

  • The Event Source request has already fired from our client-side
  • We want to intercept the URL and retrieve the cryptocurrency coin that has been sent over via the query parameters:
 console.log("Requested url: " + request.url);
    var url_parts = url.parse(request.url, true);
    var query = url_parts.query.coin
  • Once we know that we have the correct URL we want to set the correct headers of the response we are sending back:
  • This is incredibly important as the following headers ensure that the connection stays open and that the data to be returned is processed as an event stream.
 response.writeHead(200, {
            Connection: "keep-alive",
            "Content-Type": "text/event-stream",
            "Cache-Control": "no-cache",
            "Access-Control-Allow-Origin": "*"
          });
  • The Cache-Control header ensures that we don’t store data into its local cache. We want a new stream item to be consumed by our client and not something that previously read.
  • The Access-Control-Allow-Origin  gives the authorization to access external domains. This is not a production-ready approach and is just the purposes of this demo

Please note that in this specific example we are querying an external API every 10 seconds on the server to stream data back to our client. This is done with the created Nodejs timer.

Is it more common to use an EventStream to stream data from your own database. Frequently pinging an external URL that isn’t set up for many requests can be problematic if it isn’t a premium service. You may get locked out from that endpoint if you are causing a heavy load.

With that caveat in mind, we want to set up a Nodejs timer that will query the CryptoCompare endpoint every 10 seconds. The function that contains the logic to request this data is in the following getCoins method:

timer = setInterval(() => {
            response.write("\n\n");
            getCoins(query).then(res => {
                response.write(`data: ${JSON.stringify(res)}`);
                response.write("\n\n");
                console.log(JSON.stringify(res));
        }) }, 10000);

The method getCoins() returns a Promise that will resolve once our request has retrieved the values of the specified coin that was originally sent over in the query parameters. It will get these values in USD and EUR.

function getCoins(coin){
    return new Promise(function(resolve, reject) {
        https.get(
            `https://min-api.cryptocompare.com/data/price?fsym=${coin}&tsyms=USD,EUR`, (res) => {
    const { statusCode } = res;
    const contentType = res.headers['content-type'];

    let error;
    if (statusCode !== 200) {
      error = new Error('Request Failed.\n' +
                        `Status Code: ${statusCode}`);
    } else if (!/^application\/json/.test(contentType)) {
      error = new Error('Invalid content-type.\n' +
                        `Expected application/json but received ${contentType}`);
    }
    if (error) {
      console.error(error.message);
      res.resume();
      return;
    }
  
    res.setEncoding('utf8');
    let rawData = '';
    res.on('data', (chunk) => { rawData += chunk; });
    res.on('end', () => {
      try { 
        const parsedData = JSON.parse(rawData);
        //console.log(parsedData);
        resolve(parsedData);
      } catch (e) {
        console.error(e.message);
      }
    });
  }).on('error', (e) => {
    console.error(`Received error: ${e.message}`);
  });
     });
} 

Once complete, we will return these new updated values into our response and parse them.

  getCoins(query).then(res => {
                response.write(`data: ${JSON.stringify(res)}`);
                response.write("\n\n");
                console.log(JSON.stringify(res));

Remember, that each of these coin updates is happening within the one HTTP request. If we examine the network tab our EventStream will look like this:

Nodejs timer

 

Our React component will consume them in the onmessage listener.

this.eventSource.onmessage = e =>
    this.updateCoins(JSON.parse(e.data));

This calls updateCoins which will update the price state of the component – which will cause the UI to rerender and display the updated value!

 updateCoins(prices: any) {
   this.getPriceChange(prices.EUR)
   this.setState(Object.assign({}, { selectedCoinPrice: prices.EUR }));
  }

We can also check if the new price has increased or decreased using logic inside getPriceChange(price.EUR).

 getPriceChange(price: string) {
    const { selectedCoinPrice } = this.state;
    let priceIncreased: boolean = false;
    price > selectedCoinPrice ? priceIncreased = true : priceIncreased = false;
    this.setState({ priceIncrease: priceIncreased })
  }

In our JSX markup, we can then apply appropriate styles based on whether the price of the coin has gone up or down.

<span className={`${priceIncrease ? 'increase' : 'decrease'}`}>  {priceIncrease ? '?' : '?'} {selectedCoinPrice ? selectedCoinPrice : 'No Price' } </span>

Closing the Node Stream

It is important to close this Event Source connection when the component no longer needs it. Otherwise, the node stream will continuously query our crypto endpoint and continue to append it to the EventStream. This is inefficient as we are no longer using the data. Having a continous Node buffer will drain our apps resource. So, when the component unmounts we can close the connection

  componentWillUnmount() {
     if(this.eventSource)
     this.eventSource.close();
  }

Our server logic will detect this and we will end the open connection and clear the interval timer to stop requesting external data.,

response.on('close', () => {
    if (!response.finished) {
      console.log("CLOSED");
      clearInterval(timer);
      response.writeHead(404);
    }
  });

Now, if we run the command:

node src/server/server.js

And refresh our application, our server will send updates of the specified coin every 10 seconds.

Server Sent Events

Nodejs Real-time Apps

dynamic website design

 

Developing real-time applications isn’t always straightforward. However, we can achieve this dynamic website design using the server-sent events outlined in this example.

Sending server-side events to a client on a consistent basis requires some extra development time. However, it really is the most efficient way to update your UI in real-time. It is logical that data that updates every few seconds should be served over a single HTTP connection.

Using event source in Nodejs can be achieved without reaching for external packages. Sometimes Node’s built-in functionality gets overlooked for other libraries but it is capable of creating real-time data connections with relative ease.

I hope this example helps you achieve you fine-tune your real-time applications!

Resources

Github – https://github.com/garethgd/crypto-barchart-example/tree/event-source

Web Development – https://github.com/garethgd/crypto-barchart-example/tree/event-source

Proudly published with Gatsby