React: Managing Websockets with Redux and Context

Connecting components to live Socket.io events with React Context and Redux store

This article explores using Websockets with React to provide real-time feedback within components. In order to achieve an efficient state management set up, we will explore both Redux and React Context to hold specific pieces of data, that will be displayed in various components within an app’s component tree.

Before diving into some source code, let’s cover exactly how we can utilise React Context and a Redux store for up-to-date state data. A full Github Gist of the solution discussed is included at the end of the article.

React Context vs Redux Store

React Context Providers can work hand-in-hand with a Redux store for project setups that rely on both state management solutions. This happens to be the case where we use websockets to fetch some sort of real-time data, such as market price data.

Let’s assume from here that we are fetching price data for a range of assets, traded under a range of currencies:

The response received from our websocket is a typical JSON object, that collates each market with its assets, along with their values:

// price data for a range of markets, in JSON{
"markets": {
"usd": {
"prices: {
"oil": 10.00,
"gas": 12.12,
...
},
"stats": {
"total_market_cap": 213120310.00
}
},
...
},
"stats": {
"total_assets": 5,
...
}
}

Metadata is also commonly fetched from services — in this case we have stats such as the total assets being traded. This is what we can typically expect from this kind of verbose response data.

In typical Websocket fashion, this object will be provided whenever there is a price movement. Once this data comes into our React project, we need to process it accordingly.

Do do so, the components we are aiming to include are:

  • A Context Provider, for storing all the price data as it comes in from the websocket. This data will be in a raw format, hosting one big JSON object for asset prices for each market
  • A Redux Store, responsible for storing smaller pieces of key market data that may be displayed throughout your application in other components

The way we can separate these two solutions is by storing the bulk market data in a React Context, and cherry-pick pieces of other statistics from that data to update any Redux store values for other components.

Firstly, we can connect to a live websocket via Socket.io and send updated price data to the app every few seconds — let’s say our server checks for updates every 3 seconds. This data can be stored in a Context Provider, and displayed in a range of components, such as a <LiveAssetTable /> component:

Once this Context +Websocket combo is fetching data consistently, we can introduce Redux into the mix. So what we will also do within the Context component is collect statistics derived from the price data, and store those stats within a Redux store for other components to utilise:

As the Context component will also utilise its own state, any changes to these data sources will result in component re-renders, keeping your UI up to date.

While it is possible to only rely on a Context, or only rely on a Redux store to manage such a requirement, the reality is that most apps utilising global state will already have a Redux store configured. Instead of adding a bulk of additional boilerplate into the store, separating data into a Context may offer a simplified solution, keeping your data compartmentalised and more manageable.

On the flip-side, Context is a great way to have some sort of global state feature for an app that is not currently utilising Redux.

The end result of this setup is the Context component updating our app state and triggering connected-component re-renders, keeping our UI up to date:

With this theoretical understanding of how the app works under our belts, let’s next introduce some source. We’ll discuss the implementations with the following sections:

  • Setting up a <SocketManager /> component, responsible for defining the context and context provider. This provider will be wrapped around the root <App /> component, giving the entire component tree access to the context
  • Introducing a Redux store into the data flow, and updating the store with market statistics for other components to leverage
  • Connecting other component to our Redux store to display that data

Let’s start where the data flow starts — with our Websocket-managed Context Provider component, <SocketManager />.

Socket Manager: Websocket + Context Provider

The <SocketManager /> component can be thought of as the engine of our Websocket and state management solution. We will talk through this solution here and provide the full Github Gist thereafter.

Within <SocketManager /> we want to define the following things:

  • The React context itself, and the useContext() hook, giving developers the option to use the context in functional components, too
  • The <SocketManager /> component itself, that is used to wrap the rest of the application, providing the Context Provider to the entire component tree
  • Our websocket connection is managed within <SocketManager />’s component lifecycle methods. We’ll connect to the websocket when the component is initialised, and disconnect when it is unmounted
  • New socket events will update <SocketManager />’s state, and will therefore update the Context value, keeping the market data up to date

For such a setup, we require the socket.io-client package, along with react-redux for later use. Install them in your project directory:

// install dependenciesyarn add socket.io-client redux react-redux

This setup assumes that you are also using the socket.io server client, run with NodeJS, whose job it is to feed your application the market data. The backend solution for this project is for another piece, but it is important to note that socket.io provide both the server side and client side APIs for websockets.

Defining the Context itself is simple, also configuring the context hook to be exportable for functional components to leverage:

import React from "react";
import io from 'socket.io-client';
// defining the context with empty prices objectexport const SocketContext = React.createContext({
prices: {}
});
// defining a useWebsocket hook for functional componentsexport const useWebsocket = () => React.useContext(SocketContext);

Straight away we are allowing other components access to our context, and therefore making the live market price feed reachable.

Within class components, we refer to the context using static contextType property on a class:

import { SocketContext } from '../SocketManager';export const MyClassComponent extends React.Component {
static contextType = SocketContext;
...
}

Whereas within functional components we can use our newly defined useSocket() hook:

import { useWebsocket } from '../SocketManager';const MyFunctionalComponent = () => {
const priceData = useWebsocket();
...
}

Whether your project strictly adheres to class or functional component, this setup has you covered.

It is important to highlight that <SocketManager /> has its own internal state, that will dictate what our Context Provider will hold. Let’s firstly set up this boilerplate:

export class SocketManager extends React.Component {  state = {
prices: {}
}
socket = null;
...
}

A public socket class property has also been initialised to null. This property will be updated with the socket.io connection in the class constructor.

Class properties are accessible from any class function, including lifecycle methods, render and other custom methods. This is the perfect for a socket connection, whereby lifecycle methods and render require it.

If you are adopting this solution in a Typescript environment, you could also make this property private, protecting it from being mutated externally:

// Typescript friendly socket class propertysocket: SocketIOClient.Socket | null = null;

<SocketManager />’s render() will simply return the component’s children wrapped with the Context Provider:

render () {
return (
<SocketContext.Provider value={{
prices: this.state.prices
}}>

{this.props.children}
</SocketContext.Provider>
);
}

Notice that, as we mentioned above, <SocketManager />’s state dictates the value of our Context Provider, keeping websocket data fresh as the socket feeds in new price updates.

As you may have guessed, we can now wrap the entire app with <SocketManager />, or individual clusters of components we wish to give context to:

// `App` root component import { SocketManager } from './SocketManager';...return(
<SocketManager>
<App />
</SocketManager />
);

If you know exactly which part of your app needs to leverage <SocketManager />, it will make more sense to wrap the isolated components rather than the entire application.

Now for configuring real-time updates, we can utilise the classes constructor() method for websocket initialisation.

Once we have a connected socket, we can listen for the receive prices event, where the state update can happen:

constructor (props) {
super(props);
this.socket = io.connect(
process.env.NODE_ENV === 'development'
? `https://localhost:3002/`
: `https://api.mydomain.com/`
, {
transports: ['websocket'],
rejectUnauthorized: false,
secure: true
});

this.socket.on('receive prices', (payload) => {
this.setState({
prices: payload.markets
});
});
}

Examining this snippet in more detail, we have firstly initialised the socket class property with io.connect(). Notice how we have relied on the process.env.NODE_ENV environment variable to determine which endpoint to connect to:

// connect to localhost in a development environment

process.env.NODE_ENV === 'development'
? `https://localhost:3002/`
: `https://api.mydomain.com/`

We’ll discuss how to set up an encrypted websocket connection, both in development and production — relying on an Nginx Proxy — in another piece. Nonetheless, this little snippet is handy for development purposes.

Defaulting to the production URL can also be handy in development if you want to feed in live data to your development build!

Your websocket may default to the polling method if no transport config is provided. In the above snippet we only want to connect via websocket.

In my experience polling can initialise quicker, resulting in a quicker initial response from the server. However, websocket provides us a live connection in a stable manner that is more efficient in production environments.

Finally, the receive prices event is being listened to, that will update our state upon the even being triggered:

this.socket.on('receive prices', (payload) => {
this.setState({
prices: payload.markets
});
});

componentWillUnmount() is a great place to disconnect websockets. As there is a chance the socket may already be disconnected for whatever reason, wrap the socket.disconnect() method in a try catch statement:

componentWillUnmount () {
try {
this.socket !== null && this.socket.disconnect();
} catch (e) {
// socket not connected
}
}

In the event you only wish your websocket to connect on certain pages of your app, you can always refer to react-router-dom’s location property. Simply wrap <SocketManager /> with the withRouter() HOC provided by the package:

import { withRouter } from 'react-router-dom';// component snippedexport default withRouter(SocketManager);

Let’s now say we only wish to connect our Websocket on the landing page of our app, we can test the pathname value of location to do so, within constructor():

constructor (props) {
super(props);
if (this.props.location.pathname === '/') {
...
}
}

Or simply return false if we are not on the landing page, returning before the rest of the function is executed:

constructor (props) {
super(props);
if (this.props.location.pathname !== '/') {
return false;
}
...
}

You’re not limited to location properties — the component’s props can also be tested to determine whether to initialise the websocket connection.

With this covered, <SocketManager /> is now supplying components access to live market prices. Now let’s go the extra step and integrate Redux into this setup.

Integrating Redux with Websocket Events

Now let’s expand the above with Redux integration. Let’s say we wish to take a statistic — the global total market cap of the assets, from a receive prices event, this time leveraging the stats object instead of the prices object. Instead of relying on our context, we can also inject data into an app’s Redux store too.

Let’s quickly cover a action reducer pair to get our example working, firstly introducing an updateTotalMarketCap action:

// src/actions/index.jsexport const updateTotalMarketCap = val => ({
type: 'TOTAL_MARKETCAP',
total_market_cap: val
});

And include a reducer to handle this action:

// src/reducers/market.jsexport const market = (state = {}, action) => {  switch (action.type) {    case 'TOTAL_MARKETCAP':
return Object.assign({}, state, {
stats: {
totalMarketcap: action.total_market_cap
}
});
default:
return state;
}
}
export default market;

We are assuming that our Redux store here stores the total market cap at stats.totalMarketcap. We’ll also want to utilise combineReducers for friendly Redux expansion:

// src/reducers/index.jsimport { combineReducers } from 'redux';
import { market } from './market';
export default combineReducers({
market,
});

Let’s finally wrap a Redux store around the application:

// src/index.jsimport { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './reducers';
...
const store = createStore(rootReducer, {
stats: {},
});
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
, document.getElementById('root'));

The initial value of a store may not be too significant to your app, but it is good practice to define a base structure so fellow developers are aware of what the store is managing. This is more strict with Typescript based projects, where it is good practice for store structures to conform to interfaces, for additional clarity and less probability of bugs.

This should be enough boilerplate to get our total market cap persisted in a Redux solution. Let’s move on to integrating dispatch functions inside <SocketManager /> next.

What we’ll do here is link up Redux to <SocketManager /> by wrapping <SocketManager />with a Container Component, courtesy of Redux’s connect() method.

Simply put, Redux’s connect() function connects a React component to a Redux store. We can pass props and dispatch methods via the two parameters provided, that you may be aware of as mapStateToProps and mapDispatchToProps.

By doing this, we can pass existing state dispatch functions — functions that takes an action and calls our reducers to update the Redux store in question — directly into <SocketManager /> as props.

Let’s expand <SocketManager /> with the additional Redux imports needed to update the total market cap:

import { connect } from 'react-redux';
import { updateTotalMarketCap } from '../actions';

Now let’s rename the component itself, to reflect that it will be wrapped with a container component:

// before 
export class SocketManager extends React.Component {
...
//after
export class WrappedSocketManager extends React.Component {
...

And let’s finally define the container component, with the connect() function mentioned earlier:

const mapDispatchToProps = {
updateTotalMarketCap
};
export const SocketManager = connect(
null,
mapDispatchToProps
)(WrappedSocketManager);
export default SocketManager;

SocketManager is still the default component being exported, but this export statement is now referring to the container component we just defined. We’re bringing in the updateTotalMarketCap action as props, and leaving the mapStateAsProps field — the first argument of connect() — as null.

The last piece of the puzzle is to call our injected dispatch function when a new websocket event is fired. We can slot this piece of logic where we update <SocketManager />’s state, inside the receive prices event:

// updated `receive prices` websocket eventthis.socket.on('receive prices', (payload) => { // Redux store updates
this.props.updateTotalMarketCap(payload.stats.total_market_cap);
// Component state updates
this.setState({
prices: payload.markets
});
});

And now we have Context and Redux updates working hand-in-hand.

The connect() process we covered above can be repeated for any component you wish to connect your Redux store to.

For example, if we wanted to bring in an existing stats.totalMarketCap value into a component, we can utilise connect()'s first mapStateToProps parameter:

// injecting state into other componentsconst mapStateToProps = (state, ownProps) => {
totalMarketCap: state.stats.totalMarketCap,
};
export const MyComponent = connect(
mapStateToProps,
null
)(WrappedMyComponent);
export default MyComponent;

Of course, nothing is stopping you from also using matchDispatchToProps, too.

Conclusion + Github Gist

This has been a walkthrough of using multiple means of state management in line with Websocket event data. We have adopted the use case of live market prices here, a common use case for such a setup.

This is a flexible solution that does not rely on an application to already have Redux installed, with the option to fall back to the Context if Redux is not needed for the app.

To continue reading about Websockets on the backend with NodeJS, check out my article dedicated to setting up an encrypted Websocket service:

To apply the knowledge in this article to a real-world solution, check out another article focussed on building SVG price charts that handle real-time price data:

The following Gist covers <SocketManager />’s full implementation:

Programmer and Author. Director @ JKRBInvestments.com. Creator of LearnChineseGrammar.com for iOS.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store