Menu Close Home

GitHub

Twitter

Email

About

Menu About Us

Projects

Blog 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: 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

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

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.

callback method from the 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



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("



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



"); 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

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.

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("



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



"); 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.

' + `Status Code: ${statusCode}`); } else if (!/^application\/json/.test(contentType)) { error = new Error('Invalid content-type.

' + `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("



"); 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: