Making WebSocket in Sync With User Internet Connectivity in React Using Redux Part 2
Tags: javascript, react, webdev, node
Published: 2019-06-01 15:43:47


In [part one](https://jsmanifest.com/making-websocket-in-sync-with-internet-in-react/) of this series we configured a react application with redux and defined our state structure for internet state updates along with actions that invoke the updates. We created a `useInternet` hook to register the necessary event handlers to allow the app to invoke actions to change and update the state accordingly.

In this tutorial we will go ahead and enhance the app further by implementing WebSocket functionality to the app. We will make sure that when there are changes in the user's internet connectivity, the websocket client will stay in sync and respond appropriately.

Additionally we will add some extra UX after the websocket client unexpectedly closes. When the websocket client closes unexpectedly, we will make it revive itself.

_Note_: This tutorial requires you to understand a little about the [websocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) API.

_Note #2_: If you want to download the source code for this tutorial you can go ahead and clone it from the [repository](https://github.com/jsmanifest/ws-online-sync).

## Create the reducers

We will start by creating the reducers for the websocket state updates. If you remember from part one of this tutorial we coded something like this:

```jsx
import { INTERNET_ONLINE, INTERNET_OFFLINE } from '../actions'

const initialState = {
  internet: {
    isOnline: true,
  },
  ws: {
    connecting: false,
    opened: false,
  },
}

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
```

Looking at the the `ws` state slice we need to have a component listening to `ws.opened` which will change when our websocket client opens or closes.

We'll begin by creating a custom `useWebsocket` hook and importing the `useSelector` function from redux to listen for those updates:

> src/hooks/useWebsocket.js

```jsx
import { useSelector } from 'react-redux'

const useWebsocket = ({ isOnline }) => {
  const opened = useSelector((state) => state.app.ws.opened)

  return {
    opened,
  }
}

export default useWebsocket
```

We'll then create a UI component that will receive `opened` as a prop:

> src/components/WebsocketConnection.js

```jsx
import React from 'react'
import './styles.css'

const StatusMessage = ({ opened }) => (
  <h5>Your websocket is {opened ? 'opened' : 'not opened'}</h5>
)

const BodyContent = ({ opened }) => (
  <div>
    <p>
      {opened && 'Now go do stuff and have an amazing day!'}
      {!opened &&
        "You can't do anything right now. Make yourself a cup of coffee in the mean time."}
    </p>
  </div>
)

const WebsocketConnection = ({ opened }) => {
  return (
    <div className='wsc-container'>
      <div>
        <StatusMessage opened={opened} />
        <BodyContent opened={opened} />
      </div>
    </div>
  )
}

export default WebsocketConnection
```

Using the `App.js` component from the last tutorial, we will use this to import the `useWebsocket` hook we just created so we can grab the `opened` state and pass it over to our UI component we just created:

> src/App.js

```jsx
import React, { useEffect } from 'react'
import useInternet from './hooks/useInternet'
import useWebsocket from './hooks/useWebsocket'
import './App.css'
import WebsocketConnection from './components/WebsocketConnection'

const App = () => {
  const { isOnline } = useInternet()
  const { opened } = useWebsocket({ isOnline })

  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 />
      <WebsocketConnection opened={opened} />
    </div>
  )
}

export default App
```

I went ahead and applied some quick CSS styles to make it look a little appealing. I provided them here if you want to use them too:

> src/components/styles.css

```css
div.wsc-container {
  padding: 35px;
  display: flex;
  align-items: center;
  justify-content: center;
}

div.wsc-container > div:first-child {
  text-align: center;
}
```

And this is what we have now:

![your websocket is not opened. make yourself a cup of coffee in the mean time](https://jsmanifest.s3-us-west-1.amazonaws.com/posts/making-websocket-in-sync-with-internet-in-react-part-two/1.jpg)

At the moment our components won't do anything when the websocket client opens besides what's shown in the screen. That's because we haven't applied action creators for the reducers yet.

First we will create three constants for the action creators:

```jsx
export const WS_CONNECTING = 'WS_CONNECTING'
export const WS_OPENED = 'WS_OPENED'
export const WS_CLOSED = 'WS_CLOSED'
```

Then, we're going to need to create the three action creators so that the reducers will be able to communicated with:

> src/actions/index.js

```jsx
export const INTERNET_ONLINE = 'INTERNET_ONLINE'
export const INTERNET_OFFLINE = 'INTERNET_OFFLINE'
export const WS_CONNECTING = 'WS_CONNECTING'
export const WS_OPENED = 'WS_OPENED'
export const WS_CLOSED = 'WS_CLOSED'

export const internetOnline = () => ({
  type: INTERNET_ONLINE,
})

export const internetOffline = () => ({
  type: INTERNET_OFFLINE,
})

export const wsConnecting = () => ({
  type: WS_CONNECTING,
})

export const wsOpened = () => ({
  type: WS_OPENED,
})

export const wsClosed = () => ({
  type: WS_CLOSED,
})
```

With these set up, we can now go to our reducers file and import these three constants:

> src/reducers/appReducers.js

```jsx
import {
  INTERNET_ONLINE,
  INTERNET_OFFLINE,
  WS_CONNECTING,
  WS_OPENED,
  WS_CLOSED,
} from '../actions'

const initialState = {
  internet: {
    isOnline: true,
  },
  ws: {
    connecting: false,
    opened: false,
  },
}

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
```

It will go ahead and define the three constants in the switch statement to calculate the next state when called:

```jsx
case WS_CONNECTING:
  return { ...state, ws: { ...state.ws, connecting: true } }
```

```jsx
case WS_OPENED:
  return { ...state, ws: { ...state.ws, connecting: false, opened: true } }
```

```jsx
case WS_CLOSED:
  return { ...state, ws: { ...state.ws, connecting: false, opened: false } }
```

```jsx
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 } }
    case WS_CONNECTING:
      return { ...state, ws: { ...state.ws, connecting: true } }
    case WS_OPENED:
      return { ...state, ws: { ...state.ws, connecting: false, opened: true } }
    case WS_CLOSED:
      return { ...state, ws: { ...state.ws, connecting: false, opened: false } }
    default:
      return state
  }
}
```

All is good! So far we went ahead and connected the UI with the reducer state, then we created the action creators that will help dispatch actions to the reducers. The reducers will pick up from there and calculate the next state so that the components can update.

What we need to do now is to instantiate a websocket client instance. However, it needs a websocket url to connect to. For the sake of this tutorial I provided a public one we can use:

```jsx
const wsUrl = 'wss://echo.websocket.org'
```

You will be able to create and leave a websocket connection opened by passing that URL to the constructor. We will go ahead and create a `wsRef` which will be assigned a `useRef`. This is where we will attach the websocket client instance with.

> src/hooks/useWebsocket.js

```jsx
import { useRef, useEffect } from 'react'
import { useSelector } from 'react-redux'

const wsUrl = 'wss://echo.websocket.org'

const useWebsocket = ({ isOnline }) => {
  const opened = useSelector((state) => state.app.ws.opened)

  const wsRef = useRef()

  // Initiates the websocket client on mount
  useEffect(() => {
    if (!wsRef.current) {
      wsRef.current = new WebSocket(wsUrl)
    }
  }, [])

  return {
    opened,
    ws: wsRef.current,
  }
}

export default useWebsocket
```

We went ahead and created a `useEffect` which will automatically instantiate and attach a new websocket client on `wsRef` so that we can see the results on initial load.

For convenience, I provided a method for instantiating a websocket client and one for ending the currently opened websocket client:

```jsx
const initWebsocket = () => {
  if (wsRef.current) {
    wsRef.current.close()
  }
  wsRef.current = new WebSocket(wsUrl)
}
```

```jsx
const endWebsocket = () => {
  if (wsRef.current) {
    wsRef.current.close()
  }
}
```

Result:

> src/hooks/useWebsocket.js

```jsx
import { useRef, useEffect } from 'react'
import { useSelector } from 'react-redux'

const wsUrl = 'wss://echo.websocket.org'

const useWebsocket = ({ isOnline }) => {
  const opened = useSelector((state) => state.app.ws.opened)

  const wsRef = useRef()

  const initWebsocket = () => {
    if (wsRef.current) {
      wsRef.current.close()
    }
    wsRef.current = new WebSocket(wsUrl)
  }

  const endWebsocket = () => {
    if (wsRef.current) {
      wsRef.current.close()
    }
  }

  // Initiates the websocket client on mount
  useEffect(() => {
    if (!wsRef.current) {
      wsRef.current = new WebSocket(wsUrl)
    }
  }, [])

  return {
    ws: wsRef.current,
    opened,
    initWebsocket,
    endWebsocket,
  }
}

export default useWebsocket
```

We're going to add two extra buttons to the UI so that we can support functionality where the user can manually open/close the websocket client (this feature is not really used in this tutorial, but it can easily be possible). One of them will be used to initiate a new websocket client instance when clicked, and the other will end it:

> src/App.js

```jsx
const { ws, opened, initWebsocket, endWebsocket } = useWebsocket({ isOnline })
```

```jsx
<div className='button-controls'>
  <button type='button' onClick={initWebsocket}>
    Initiate Websocket
  </button>
  <button type='button' onClick={endWebsocket}>
    End Websocket
  </button>
</div>
```

Great!

But wait. We created a way for the components to update, but they need a place and time to be updated from.

We'll go back to our `initWebsocket` function and attach some event listeners to the `open` and `close` events:

> src/hooks/useWebsocket.js

```jsx
const initWebsocket = () => {
  if (wsRef.current) wsRef.current.close()
  wsRef.current = new WebSocket(wsUrl)
  wsRef.current.addEventListener('message', () => {})
  wsRef.current.addEventListener('open', () => {})
  wsRef.current.addEventListener('close', () => {})
  wsRef.current.addEventListener('error', () => {})
}
```

Remember that a websocket connection can have four different listeners:

| Listener  | Description                                                         |
| --------- | ------------------------------------------------------------------- |
| onclose   | Called when the WebSocket connection's readyState changes to CLOSED |
| onmessage | Called when a message is received from the server                   |
| onopen    | Called when the WebSocket connection's readyState changes to OPEN   |
| onerror   | Called when an error occurs on the WebSocket                        |

         |

With this in place we can now look forward to attach some handlers:

```jsx
const onMessage = (msg) => {
  console.log(msg)
}

const onOpen = () => {
  console.log('WS client opened')
}

const onClose = () => {
  console.log('WS client closed')
}

const onError = () => {
  console.log('WS client errored')
}

const initWebsocket = () => {
  if (wsRef.current) wsRef.current.close()
  wsRef.current = new WebSocket(wsUrl)
  wsRef.current.addEventListener('message', onMessage)
  wsRef.current.addEventListener('open', onOpen)
  wsRef.current.addEventListener('close', onClose)
  wsRef.current.addEventListener('error', onError)
}
```

Currently our _useEffect_ is attaching a new websocket client instance on _wsRef.current_ but it's now missing the implementation of registering event handlers. So we need to do a quick update to make it invoke the handler that does the registering instead:

> src/hooks/useWebsocket.js

```jsx
// Initiates the websocket client on mount
useEffect(() => {
  if (!wsRef.current) initWebsocket()
}, [initWebsocket])
```

Also, since we registered the event listeners in the initiator handler, we also need to make sure that they get removed when the client is closed, to avoid a memory leak:

> src/hooks/useWebsocket.js

```jsx
const endWebsocket = () => {
  if (wsRef.current) {
    wsRef.current.removeEventListener('message', onMessage)
    wsRef.current.removeEventListener('open', onOpen)
    wsRef.current.removeEventListener('close', onClose)
    wsRef.current.removeEventListener('error', onError)
    wsRef.current.close()
  }
}
```

From the beginning, our goal was to make the _websocket in sync with the user's internet connectivity_. Looking at what we got so far in our code, we now have an API set up to allow us to approach that functionality.

When the the user's internet goes offline, our _websocket `close` event handler must be invoked_ somehow.

The thing is, the websocket client is _not in sync_ with the internet connection. I've tested this on linux and windows machines and when the internet goes off the websocket client's _[readyState](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState)_ property can still be stuck at _1_ (the value for the client's _OPEN_ state). So we cannot rely on it to close by itself.

For a quick reverence of the different _readyState_'s' you can either click the link above or have a look at this table:

Remember that a websocket can go through [four different states](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState#Value) throughout the lifetime of their connection:

| Value | State      | Description                                              |
| ----- | ---------- | -------------------------------------------------------- |
| 0     | CONNECTING | Socket has been created. The connection is not yet open. |
| 1     | OPEN       | The connection is open and ready to communicate.         |
| 2     | CLOSING    | The connection is in the process of closing.             |
| 3     | CLOSED     | The connection is closed or couldn't be opened.          |

If the internet goes offline then we need to invoke the websocket client's _close_ handler immediatel. When it comes back online we need to do the same for the _open_ handler, otherwise the websocket client will show that the user is still connected even though his internet is disconnected. Very misleading! We should fix that.

Going back to the action creators we created earlier, we can utilize them to send a signal to our reducers:

> src/actions/index.js

```jsx
export const wsConnecting = () => ({
  type: WS_CONNECTING,
})

export const wsOpened = () => ({
  type: WS_OPENED,
})

export const wsClosed = () => ({
  type: WS_CLOSED,
})
```

And here is the final update to our reducers:

> src/reducers/appReducers.js

```jsx
import {
  INTERNET_ONLINE,
  INTERNET_OFFLINE,
  WS_CONNECTING,
  WS_OPENED,
  WS_CLOSED,
} from '../actions'

const initialState = {
  internet: {
    isOnline: true,
  },
  ws: {
    connecting: false,
    opened: false,
  },
}

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 } }
    case WS_CONNECTING:
      return { ...state, ws: { ...state.ws, connecting: true } }
    case WS_OPENED:
      return { ...state, ws: { ...state.ws, connecting: false, opened: true } }
    case WS_CLOSED:
      return { ...state, ws: { ...state.ws, connecting: false, opened: false } }
    default:
      return state
  }
}

export default appReducer
```

Our reducers are all hooked up with the action creators and now they should be updating. Our next step is to make the UI components update. We will modify the _useWebsocket_ hook to invoke some handlers so that our UI component will just update--not having to worry about any logic. Doing so will make our code much easier to read and maintain in the future.

Inside our hook we are going to make another _useEffect_ which will invoke everytime the value of _isOnline_ changes. When _isOnline_ changes to `false`, we'll go ahead and dispatch the _wsClosed_ action. When it changes to `true` we will dispatch the _wsOpened_ action. Doing so will make all of the components that are attached to the state update accordingly to the changes.

> src/hooks/useWebsocket.js

```jsx
// Responsible for updating redux when isOnline changes
useEffect(() => {
  if (isOnline && !opened) {
    dispatchAction(wsOpened())
  } else if (!isOnline && opened) {
    dispatchAction(wsClosed())
  }
}, [isOnline, dispatchAction, opened])
```

In addition, we also need to dispatch the _wsClosed_ action in the _endWebsocket_ function to help the *useEffect*s to stay in sync when there are changes to the user's internet connection:

> src/hooks/useWebsocket.js

```jsx
const endWebsocket = () => {
  if (wsRef.current) {
    wsRef.current.removeEventListener('message', onMessage)
    wsRef.current.removeEventListener('open', onOpen)
    wsRef.current.removeEventListener('close', onClose)
    wsRef.current.removeEventListener('error', onError)
    wsRef.current.close()
    if (opened) dispatchAction(wsClosed())
  }
}
```

Our state in redux should now update and attempt to keep the websocket client in sync. However, the websocket client won't just close automatically yet. We need to _make it_ close by invoking the _[close](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close)_ method in the onClose handler:

> src/hooks/useWebsocket.js

```jsx
import { useDispatch, useSelector } from 'react-redux'
import { wsOpened, wsClosed } from '../actions'
```

```jsx
const dispatchAction = useDispatch()
```

```jsx
const onOpen = (e) => {
  console.log('WS client opened')
}
```

```jsx
const onClose = (e) => {
  console.log('WS client closed')
  if (wsRef.current) {
    wsRef.current.close()
  }
}
```

Since we attached the action creators to the _open_ and _close_ event handlers for the websocket client, we can go ahead and just import the hook into a component now:

```jsx
import React, { useEffect } from 'react'
import useInternet from './hooks/useInternet'
import useWebsocket from './hooks/useWebsocket'
import WebsocketConnection from './components/WebsocketConnection'
import './App.css'

const App = () => {
  const { isOnline } = useInternet()
  const { ws, opened, initWebsocket, endWebsocket } = useWebsocket({ isOnline })

  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 />
      <WebsocketConnection opened={opened} />
      <div className='button-controls'>
        <button type='button' onClick={initWebsocket}>
          Initiate Websocket
        </button>
        <button type='button' onClick={endWebsocket}>
          End Websocket
        </button>
      </div>
    </div>
  )
}

export default App
```

And voila! Try disconnecting your internet and see the results:

Online state:

![2](https://jsmanifest.s3-us-west-1.amazonaws.com/posts/making-websocket-in-sync-with-internet-in-react-part-two/2.JPG)

Next, I disconnected the internet:

![3](https://jsmanifest.s3-us-west-1.amazonaws.com/posts/making-websocket-in-sync-with-internet-in-react-part-two/3.JPG)

Connected back to the internet:

![4](https://jsmanifest.s3-us-west-1.amazonaws.com/posts/making-websocket-in-sync-with-internet-in-react-part-two/4.JPG)

## Conclusion

And that concludes the end of this series!