Making WebSocket in Sync With User Internet Connectivity in React Using Redux Part 1
May 27th, 2019
When I was in charge of implementing web socket functionality at a startup company recently because of a backend change, it was not an easy adventure. Not only was I new to web sockets but a google search barely provided any solutions to a stable websocket architecture in react applications. Where would I find a tutorial then if this was the case?
Well... in the end I didn't get to use a tutorial that fit my needs. The articles that provided any good source of information were usually outdated (2017 and older) and used syntax that are now considered bad practices. JavaScript is a fast-growing community where technology changes fast. I didn't trust any tutorials older than 2018. That only made it worse for me.
So luckily I found react-websocket and took a good look at the source code. It gave me an idea where to start.
I didn't know until later that my previous implementation of websockets to the app before was unsynchronized with the users internet connection and so my boss had me fix it. Looking at react-websocket I began realizing that websocket clients can be instantiated and attached to a react component instance which is a good start to keeping the websocket connection in sync with UI updates. Seeing as how scary it is implementing a feature right into a production app to be shipped into the industry was a scary thought. So I began thinking of all the possible downsides to this approach and realized one major problem: What if the component unmounts? The websocket closes its connection. Bummer. The app I am implementing this feature to is heavily dependent on the persistence of an opened websocket connection. Once there is any sign of change in the user's network connection, I best have notified to the user or update the UX in some way immediately.
I began to play around with instantiating and attaching the websocket client onto the browser's window
object, but it did not play nicely as react did not update itself to window events. Doing all the local state management of having the user's internet synchronized with their websocket connection was a nightmare in a react component. There were a lot of unexpected infinite loops in re-rendering and potential memory leaks.
I leveraged react's new feature, context to try to provide a global state to all the child components, but this was not a great idea because it made the context component bloated with state updates from the websocket client and I had to go optimize all the child components being wrapped by it to re-render only when necessary. But that's totally unnecessary. There's a better approach.
I ended up leveraging redux to handle the state updates. The benefits were large:
When I was finished with the whole implementation and pushed it to production, my boss never mentioned a problem with the websocket again. It's been over a month since then.
This article is part 1 of 2 in the Making WebSocket in sync with the users internet connectivity in React using Redux series. This is not a post to encourage you that this is the right way to code the feature, but is simply a post where I show what worked for me and my company in a production app. If there is a more robust approach or any concerns, feel free to comment below this post!
We will use create-react-app (CRA) to quickly bootstrap a react app so we can get started with the code implementation.
Create a new CRA project and name it anything. I named it ws-online-sync
npx create-react-app ws-online-sync
Step into the directory:
cd ws-online-sync
Install redux and react-redux (Note: our examples are using react-redux v7.1.0-alpha.5
. You must install with react-redux@next to use those examples or you will get an error)
npm install --save redux react-redux@next
After we have the dependencies installed we can go ahead and clean up App.js (the component imported and rendered from the main index.js file) to look a little more cleaner:
import React from 'react'
import './App.css'
const App = () => {
return (
<div>
<h1>Making Websocket in Sync with the User's Internet Connectivity</h1>
<hr />
</div>
)
}
export default App
Since we will be using redux we must do a couple of quick configurations in order to make it work in the app.
This is the directory structure of our examples:
Our components are required to be wrapped with a Provider that is exported from the react-redux package. It takes store as a prop and makes it available throughout the lifetime of the user's client session.
The store can be instantiated and configured by importing createStore from redux.
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import './index.css'
import App from './App'
import * as serviceWorker from './serviceWorker'
import rootReducer from './reducers'
const store = createStore(rootReducer)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
)
serviceWorker.unregister()
We passed rootReducer as an argument to the createStore method. This is mandatory as it will contain our state structure for the internet and websocket states.
reducers/index.js
import { combineReducers } from 'redux'
import app from './appReducers'
export default combineReducers({
app,
})
Here is how it looks like in a directory structure:
We need that rootReducer to constantly return us the next state tree every time the user's internet and websocket client connection changes.
The redux documentation on reducers explain that "reducers specify how the application's state changes in response to actions sent to the store".
With that said, we now need to define the actions that get sent to the store in order for the reducers to update.
The standard approach to using action types is using constants, and I like the standard way so I will need to define the constants for the actions this way:
actions/index.js
export const INTERNET_ONLINE = 'INTERNET_ONLINE'
export const INTERNET_OFFLINE = 'INTERNET_OFFLINE'
We can now proceed to define the action creators:
actions/index.js
export const INTERNET_ONLINE = 'INTERNET_ONLINE'
export const INTERNET_OFFLINE = 'INTERNET_OFFLINE'
export const internetOnline = () => ({
type: INTERNET_ONLINE,
})
export const internetOffline = () => ({
type: INTERNET_OFFLINE,
})
The reducer will import these constants to use them in their switch statements:
reducers/appReducers.js
import { INTERNET_ONLINE, INTERNET_OFFLINE } from '../actions'
const initialState = {
internet: {
isOnline: true,
},
}
const appReducer = (state = initialState, action) => {
switch (action.type) {
case INTERNET_ONLINE:
return { ...state, internet: { ...state.internet, isOnline: true } }
case INTERNET_OFFLINE:
return { ...state, internet: { ...state.internet, isOnline: false } }
default:
return state
}
}
export default appReducer
Great! We now have the internet online/offline hooked up on redux and are ready to move down to the components. The components that need to know updates to this state will simply just hook itself to that state slice.
For the next upcoming examples we will be making use of the new react hooks feature--a new addition in React 16.8.
We are going to create a useInternet hook that will be used at the top where the App component is so that we can get as much UI as possible to read from it when needed.
Create a hooks folder in the src directory and create a useInternet.js file inside.
This useInternet hook will register an online
and offline
event to the global window object by using window.addEventListener
.
This is needed for any offline-capable web application and based on my experience is very effective and precise in keeping your app in sync with the user's internet connection. When the user's internet goes offline, this is where we will dispatch an action in redux so that any component in the app will update accordingly to their network connection.
hooks/useInternet.js
import { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { internetOnline, internetOffline } from '../actions'
const useInternet = () => {
const dispatchAction = useDispatch()
const isOnline = useSelector((state) => state.app.internet.isOnline)
// Registers event listeners to dispatch online/offline statuses to redux
useEffect(() => {
const handleOnline = () => {
dispatchAction(internetOnline())
}
const handleOffline = () => {
dispatchAction(internetOffline())
}
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
return function cleanup() {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
}, [dispatchAction])
return {
isOnline,
}
}
Simple, yet effective and powerful :)
The return function cleanup()
code block is recommended so that that when the component unmounts, it will avoid a memory leak in your application.
This custom hook should be fine for detecting internet connection changes, but we can secure the accuracy a little further by providing a second useEffect hook and using the navigator.onLine property from the global window object. Seeing as how it is widely supported by almost all browsers, it was an easy decision to help keep the hook more robust, accurate and useful for a production app :)
// Invokes the redux dispatchers when there is a change in the online status of the browser
useEffect(() => {
if (window.navigator.onLine && !isOnline) {
dispatchAction(internetOnline())
} else if (!window.navigator.onLine && isOnline) {
dispatchAction(internetOffline())
}
}, [dispatchAction, isOnline])
And here is the final code for the useInternet hook:
import { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { internetOnline, internetOffline } from '../actions'
const useInternet = () => {
const dispatchAction = useDispatch()
const isOnline = useSelector((state) => state.app.internet.isOnline)
// Registers event listeners to dispatch online/offline statuses to redux
useEffect(() => {
const handleOnline = () => {
dispatchAction(internetOnline())
}
const handleOffline = () => {
dispatchAction(internetOffline())
}
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
return function cleanup() {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
}, [dispatchAction])
// Invokes the redux dispatchers when there is a change in the online status of the browser
useEffect(() => {
if (window.navigator.onLine && !isOnline) {
dispatchAction(internetOnline())
} else if (!window.navigator.onLine && isOnline) {
dispatchAction(internetOffline())
}
}, [dispatchAction, isOnline])
return {
isOnline,
}
}
To test out how accurate this is, import this hook into your App.js component and provide a useEffect to react to internet connectivity changes like so:
App.js
import React, { useEffect } from 'react'
import useInternet from './hooks/useInternet'
import './App.css'
const App = () => {
const { isOnline } = useInternet()
useEffect(() => {
console.log(
`%cYou are ${isOnline ? 'online' : 'offline'}.`,
`color:${isOnline ? 'green' : 'red'}`,
)
}, [isOnline])
return (
<div>
<h1>Making Websocket in Sync with the User's Internet Connectivity</h1>
<hr />
</div>
)
}
export default App
Now run the app, open up the console in the developer tools and disconnect your internet. After turning it back on your console should show this:
And that concludes the end of part one! We have configured a redux react app and defined our state structure for internet state updates along with the actions that invoke the updates. We also created a useInternet
hook to register the event handlers and allow the app to invoke actions to make state updates accordingly.
In part two we will go ahead and implement WebSocket functionality into the app. We will make sure that when there are changes in the user's internet connectivity, the websocket will stay in sync and react accordingly. We will make sure that the WebSocket client will revive itself and "remember" the previous state of the app when the user's internet goes offline. We will also make sure that the web socket client will try to reconnect after 3 times before giving up.
Stay tuned for Part 2!