React Hooks: Managing Web Sockets with useEffect and useState

Rundown of React Hooks and applying them to a real-time chat room simulation

Hooks in React have triggered a transition in how React developers structure their projects; a catalyst for adopting functions in place of classes. Even though Hooks are still only a proposal at the time of this writing, the social media buzz around them, as well as the major improvements in syntax simplification they adhere to, suggests they are here to stay — albeit with some tweaks to come before the proposal is pushed into a final React release.

This article will visit 2 hooks bundled into React 16.7 alpha and use them to simulate a real-time chat room environment utilising socket.io and Express, with the aim of demonstrating how these hooks work.

We will then examine the behaviour of our hooks and showcase an additional demo to improve our room joining behaviour (spoiler: bottom of article). The demos can be easily replicated, and I will list the commands for setting up the project for anyone to use after the explanation.

Without getting ahead of ourselves, let’s examine the first demo, where we will see how useState and useEffect can be used with a web socket to control the mounting, re-rendering and unmount process. The end result is the following:

Top Left: Express Server. Bottom Left: Javascript Console. Left: React App

Demo showcasing how useState and useEffect hooks manage a websocket connection

Hooks Overview

In the above demo we are using hooks in the following ways:

useState

useState is used to set our component state variables. We define these hooks as soon as we open our function declaration. No classes are used in this demo, nor do we need to use setState() for updating state:

function App() {
   const [messageCount, setMessageCount] = useState(0);
const [theme, setTheme] = useState('dark');
const [inRoom, setInRoom] = useState(false);

...

Let’s look at how a useState hook is defined, and then compare it to standard React syntax.

  • useState utilises destructuring assignment syntax (read more about it here). What this syntax allows us to do is unpack values from an array into distinct variables. For each hook above, we are defining 2 variables in its array, which we can then use in our App function thereafter to refer to, and update, the state value.
  • The first array variable is the variable name that refers to our state value.
  • The second array variable refers to the function called to update our state value.
  • The values passed into useState() are the default state values. The default state for messageCount is 0, and the default value for theme is dark.

Consider messageCount. This is how we could define our state value and update function in a standard React class:

class App extends React.component {
   state = {
messageCount: 0
};
   const messageCount = this.state.messageCount;
   setMessageCount = count => (
setState({messageCount: count});
)
...
}

This is essentially the same as declaring a useState hook:

function App() {
const [messageCount, setMessageCount] = useState(0);
...
}

By using useState, we can see our syntax has shrunk quite dramatically, whilst maintaining readability.

That’s useState. Now let’s look at the second hook, useEffect.

useEffect

The useEffect hook serves a very different purpose to useState. It deals with component side effects — e.g. stuff that is processed through the mounting and unmounting process of a component.

In other words, useEffect is an alternative to using React class lifecycle methods; three of them in-particular: componentDidMount, componentDidUpdate, and componentWillUnmount.

How does this look syntax-wise?

  • useEffect simply takes a function as its one required argument. Everything within this argument will be run upon the componentDidMount and componentDidUpdate phases.
  • Within this function, we can return another function that acts as component cleanup upon unmounting: E.g. the componentWillUnmount phase.
  • Finally, after our function, useEffect can also take an optional array as its second argument, containing state values that must change for the re-render to take place.

These 3 characteristics of useEffect can be encapsulated in the following template:

function App() {
   useEffect(() => {
      //stuff that happens upon initial render
///and subsequent re-renders
//e.g. make a fetch request, open a socket connection
      return () => {
//stuff that happens when the component unmounts
//e.g. close socket connection
}
}, [messageCount]);

...
}

As you can see, useEffect makes components a lot easier to read and manage: they essentially group your related lifecycle code to one function, although we still have the flexibility to define multiple useEffects in one component. Defining multiple useEffects becomes valuable when using the second array argument of useEffect, which our demo demonstrates.

By now you may have already guessed how we can manage a web socket connection using only hooks.

In any case, let’s see how this is done in our demo next.

Demo App.js

Let’s visit the demo React app to solidify our understanding of these hooks. The demo mostly consists of the default create-react-app boilerplate code, with some hooks and buttons added on, and with our socket.io client for handling the web socket connection:

In quite a readable fashion, the component consists of the following:

Socket connection, connecting to localhost:3011. There is no significance to your port number — just make sure it matches up with your backend socket.io interface. We will visit the server-side code later in the article for the sake of demo completeness.

useState hooks. As discussed above, our 3 state hooks are defined straight after the function opens, unconditionally.

The first useEffect hook
Our first useEffect is responsible for managing room joining and leaving. The mounting stage emits a join room event to the backend web socket client, asking to join our test-room. We also handle incoming messages from the test-room here, by listening to receive message events that are broadcasted from our web socket server. Such an event updates increments our messageCount state value.

The return block, called when the component unmounts, conversly emits a leave room event to leave the test-room.

Notice these events only happen when our inRoom state value is true — which defaults to false on the initial render. Further down the component we have a button that toggles this state value. This allows us to enter the room and leave the room, rather than automatically join as soon as the page loads.

useEffect, like all hooks, need to be defined unconditionally, therefore we cannot do something like define the hook within if(inRoom), for example. Instead we test for conditions within the hook.

The second useEffect hook
This useEffect hook updates the tab title in the browser with our messageCount state value.

Notice that the hook is only processed on re-renders if messageCount changes. Here we have utilised the second argument of useEffect, which takes an array of state values that have to change in order for this effect to process on re-renders.

handleSetTheme, handleInRoom and handleNewMessage

The following 3 functions handle our button clicks:

  • handleSetTheme toggles our theme between light and dark, which is simply reflected as a className in render.
  • handleInRoom toggles our inRoom state variable between true and false.
  • handleNewMessage emits a new message event to our server web socket client, which in turn broadcasts the receive message event to all clients in our test-room. But did we not already handle the receive message event in our useEffect hook? Yes — but events emitted by a sender are not emitted back to the same sender. Therefore, we instead increment our messageCount state variable here.

return

Our return block renders the default create-react-app home page. A couple of headers and buttons have been added to reflect our chat room state, as well as the theme class that toggles the background colour of the page, between light and dark.

There’s quite a bit happening in this function component, but the coherency of the syntax and layout make it rather readable. I bet someone who has not endeavoured into socket.io will quickly understand what is happening here upon first look.

Express socket.io Server

To make this explanation complete, let’s briefly visit the Express server hosting the socket.io server-side logic. Within app.js, our socket.io configuration is defined below the Express boilerplate code:

As you can see, we have defined our socket.io events here corresponding to our React configuration.

Setting up the project

To run the project yourself, set up your React app and Node server with a couple of additional installations. Let’s quickly run through them.

React Setup

For the React side, generate a new app with create-react-app, before updating the React version to 16.7 alpha in order to utilise hooks.

Note: If you want to solely use yarn, do so with yarn add <package>.

create-react-app websockets
cd websockets && yarn
npm i react@16.7.0-alpha.0
npm i react-dom@16.7.0-alpha.0
npm i socket.io-client
yarn start

I am getting into the habit of changing new React projects to concurrent mode as soon as they are initiated. To do this, open index.js and change your ReactDOM.render() to:

ReactDOM.createRoot(document.getElementById('root')).render(<App />);

Finally, copy the App.js code from the above Gist. Don’t forget to define your theme colours in App.css:

.Theme-light {
background-color: #aaa;
}
.Theme-dark {
background-color: #282c34;
}

Express Setup

On the Node JS side, initiate a new Express server with a socket.io installation:

express backend_websockets
cd backend_websockets && yarn
npm i socket.io
node bin/www

Finally, copy the app.js code from above. (lower case app.js for the node side!, capitalised App.js for the React side.)

With both apps running on localhost, you are now good to go with testing.

Examining console.log

Check out what happens when we toggle our buttons.

  • A channel to localhost:3011 is persisted upon the initial page load.
  • At this stage, we are not in the test-room. Therefore, if we go ahead and click Toggle Theme, none of our room joining or leaving takes place upon re-rendering the component.
  • Now Let’s click Enter Room. Our inRoom state is toggled, and thereafter the first useEffect() hook is processed, and we join our test-room by emitting the join room event.
  • Now when we toggle the app theme state value, a re-render of the component is triggered again, but now causes us to leave and re-join the test-room. Notice though how our second useEffect() is ignoring this re-render, as the messageCount state has not changed.
  • Clicking Emit New Message also has the same effect, causing a re-render and the leaving and joining of the room again.

Additional Challenge & Solution

As an additional challenge, consider doing the following to enhance the demo:

Isolate the theme and newMessage state variables to a separate component that will NOT make us leave and re-enter the test-room upon these state variables being updated.

Solution:

Re-rending the <Messages /> component does not trigger a <Room /> re-render, allowing us to stay in the test room

The solution here is to separate the <Room /> state and <Message /> state into separate components, so our room joining and entering only happens when we toggle the inRoom state variable (and if we unmount the App altogether).

This comes at a price though, as the theme control now only applies to our Messages UI, and not the entire app (although you could get past this with some clever CSS). The updated full solution is as follows, named App2.js:

I also amended the following CSS to improve presentation, in App.css:

body {
background-color: #282c34;
color: #fff;
text-align: center;
padding: 20px;
}
.App-header {
min-height: 50vh;
}

Where to go from here

Thank you for following this demo on the two most talked about React hooks. You should now have insight of how to apply them to your own projects, and in the context of managing web socket connections.