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. This article will visit 2 hooks, useState
and useEffect
, 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. Right: React App

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 ourApp
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 formessageCount
is0
, and the default value fortheme
isdark
.
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 thecomponentDidMount
andcomponentDidUpdate
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 ourinRoom
state variable between true and false.handleNewMessage
emits anew message
event to our server web socket client, which in turn broadcasts thereceive message
event to all clients in ourtest-room
. But did we not already handle thereceive message
event in ouruseEffect
hook? Yes — but events emitted by a sender are not emitted back to the same sender. Therefore, we instead increment ourmessageCount
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 to the latest version to ensure hooks are implemented.
Note: If you want to solely use yarn, do so with yarn add <package>.
create-react-app websocketscd websockets && yarnnpm i react@latestnpm i react-dom@latestnpm i socket.io-clientyarn 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_websocketscd backend_websockets && yarnnpm i socket.ionode 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 firstuseEffect()
hook is processed, and we join ourtest-room
by emitting thejoin 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 thetest-room
. Notice though how our seconduseEffect()
is ignoring this re-render, as themessageCount
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:

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.
To read more about embedding Websockets within React projects, I have published another article focused on integrating Websocket events with React Context and Redux: