Real-time Server Sent Events with React & Event Source

Dynamic Website Design

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 crypto currency and CryptoCompare will be the source.

The application auto updates every 10 seconds and the demo is available here:dynamic website design

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.

This application can consider making multiple HTTP requests to request 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 everytime 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 open a persistant connection to a HTTP Server. It sends server side events with 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

We have a React component using TypeScript that contains the following:

  • A dropdown selector that loads a list of crypto currency coins on component mount
  • 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 crypto currency data and updates the value show 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) {
    if (coinType) this.setState({ loading: true });

    let coinToCompare = coinType ? coinType : 'BTC';
   
    const res = axios.get(
      `https://min-api.cryptocompare.com/data/price?fsym=${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 
      });
  }

  private getCoinTypes() {
    let coins: CoinInfo[] = [];

    axios
      .get(
        'https://rocky-bayou-96357.herokuapp.com/https://www.cryptocompare.com/api/data/coinlist/'
      )
      .then(response => {
        Object.keys(response.data.Data).forEach(function(key) {
          coins.push(response.data.Data[key]);
        });

        coins.sort((a, b) =>
          a.CoinName.toUpperCase().localeCompare(b.CoinName.toUpperCase())
        );

        const initialCoin = coins.find(coin => coin.Symbol === this.state.selectedCoinSymbol);

        if (initialCoin)
        this.getCoinCompare(initialCoin.Symbol);
        this.setState({
          coinTypes: coins,
          selectedCoin: initialCoin
        });
      })
      .catch(function(error) {});
  }

   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 initialises 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 setup 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 crypto currency 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 setup a Nodejs timer.

This isn’t very cumbersome as we don’t need to utilise 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 has already fired from our client-side
  • We want to intercept the url and retrieve the crypto currency coin that has been sent over in the 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:
  • 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 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 setup for many requests can be problematic if it isn’t setup as premium service. You may get locked out from that endpoint if you are causing a heavy load.

With that in mind., we want to setup 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 be 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 Buffer

dynamic website design

Developing real-time applications isn’t always straightforward. However, we can achieve this dynamic website design using 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 a streamreader and streamwriter in for a Nodejs buffer 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.

Resources

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

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

Gareth Dunne

Full Stack Developer and creator of JSdiaries. Passionate about the latest in web technologies and how it can provide value for my clients.