I was working on a project that required real-time updates. I needed to check a user's status and perform certain actions based on the user's current status.
For instance, I have a hibernation timer for each user that runs for 3 days. After the timer expires, I want to change the user's status to active, add transaction history, and perform other actions for the user.
Another example is updating user permissions from the web admin dashboard. I want the updated permissions to reflect immediately without requiring the user to re-authenticate.
I have a server running on Express.js and an endpoint, which I use to check the user status and perform all the actions I want.
My frontend uses React Js, and I had to constantly call the endpoint to see if my conditions had been met. If they had, the actions I had lined up for the user would be performed. This method is called polling.
Polling simply means checking for new data over a fixed interval of time by making API calls at regular intervals to the server. It is used to get real-time updates in applications.
Here is an example of how it was implemented:
// StatusSlice.js
export const checkStatus = createAsyncThunk(
'get/checkStatus',
async (_, thunkAPI) => {
try {
const response = await axios.patch('/user/check-status', {});
console.log(response);
} catch (error) {
return thunkAPI.rejectWithValue(error);
}
}
);
// Status.js
useEffect(() => {
dispatch(checkStatus())
.unwrap()
.then((res) => {
dispatch(updateUserDetails(res));
});
if (checkStat) setCheckStat(false);
}, [dispatch, checkStat]);
// sending request to the sendpoint every 5 secs
setInterval(() => {
setCheckStat(true);
}, 5000);
While this approach seems straightforward and clean—and it worked because I got real-time updates from the server—it repeatedly sends HTTP requests to the server at fixed intervals, asking for updates. This method wasn't ideal because If I had over 1000 users logged in simultaneously, my server would be overwhelmed by the volume of requests. Handling 1000 requests to check user status would be very expensive.
I did a bit of research and discovered that using WebSockets would be a better approach.
According to Mozilla docs, The WebSocket API is an advanced technology that makes it possible to open a two-way interactive communication session between the user's browser and a server. With this API, you can send messages to a server and receive event-driven responses without having to poll the server for a reply.
This was exactly what I needed, so I started implementing it right away.
On my backend codebase, I installed WebSocket using npm install ws
and created a new file called websocket.js
for the WebSocket configuration:
//websocket.js
const WebSocket = require('ws');
//Port to run websocket on frontend
const wss = new WebSocket.Server({ port: 8080 });
const handleError = (ws, error) => {
logger.error('WebSocket error:', error);
ws.terminate(); // Close the connection in case of errors
};
// broadcast function
wss.broadcast = function broadcast(data) {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
try {
client.send(data);
} catch (error) {
handleError(client, error);
}
}
});
};
wss.on('connection', async function connection(ws) {
logger.info('Client connected');
ws.on('error', (error) => handleError(ws, error));
// ws.on('close', () => logger.info('Client disconnected'));
// WebSocket connection handler
wss.on('connection', function connection(ws) {
console.log('Client connected');
//checks for message being received from the frontend
// User id or token can be sent here from the frontend
ws.on('message', function incoming(message) {
console.log('Received message:', message);
});
//Sends response to the frontend
ws.send(JSON.stringify({ message: 'Welcome to the WebSocket server' }));
});
I had originally written a checkStatus
function for the controller when running the endpoint, which is no longer needed in that form. Now, I want this function to run at intervals and perform specific actions based on the user's status. I have refactored this function to serve as an example for this article.
// websocket.js
const checkStatus = async (userID)=>{
try{
const user = await User.findById(userId).select('-password -token -__v');
if (!user) {
logger.error('User not found for ID:', userId);
return;
}
const now = new Date();
if(user.status === "pending" && now > new Date(user.timer){
user.status = "active"
user.timer = null
}
if(user.status === "active"){
user.goal = 1
}
return { status: 'success', user };
} catch (err) {
logger.error('Error checking user status:', err);
}
}
So, the userId
has to be passed to the checkStatus
function because I want the action to happen to individual users depending on their statuses. At the end, I return the user with their updated details. This returned user is what I'll send to the frontend.
Now, I set up a setInterval
because I want to periodically check user status:
//websocket.js
setInterval(async () => {
wss.clients.forEach(async (client) => {
logger.info('Checking status for user id:', client.userId);
if (client.userId) {
const result = await checkStatus(client.userId);
if (result.status === 'success') {
wss.broadcast(
JSON.stringify({ type: 'status_update', user: result.user })
);
} else {
logger.error('Error in checkStatus:', result.message);
}
}
});
}, 10000);
The setInterval
function periodically checks the status of connected users on the server side. Instead of clients sending requests at regular intervals like we did on the frontend, the server pushes updates to clients whenever there are any changes.
We need to set the WebSocket connection to receive userId
from the frontend and save it in the ws
object.
Here's the adjusted code:
//websocket.js
wss.on('connection', async function connection(ws) {
logger.info('Client connected');
ws.on('error', (error) => handleError(ws, error));
// ws.on('close', () => logger.info('Client disconnected'));
// WebSocket connection handler
wss.on('connection', function connection(ws) {
console.log('Client connected');
//checks for message being received from the frontend
// User id or token can be sent here from the frontend
ws.on('message', function incoming(message) {
console.log('Received message:', message);
const receivedData = JSON.parse(message)
const userID = receivedData.userId
});
if (!userId) {
ws.send(
JSON.stringify({ message: 'Missing authentication information' })
);
return;
}
try {
ws.userId = userId; // Set userId
logger.info('Authenticated user:', ws.userId);
await checkStatus(userId); // Call checkStatus here
} catch (error) {
ws.send(JSON.stringify({ message: 'Authentication failed' }));
logger.error('Authentication error:', error.message);
}
//Sends response to the frontend
ws.send(JSON.stringify({ message: 'Welcome to the WebSocket server' }));
});
I can now periodically check for my user's status and return updated user info to the frontend.
On the frontend side of things, let's install a package that helps with reconnecting and receiving data from websocket: npm install reconnecting-websocket
.
//status.js
import ReconnectingWebSocket from 'reconnecting-websocket';
const Status = () => {
const [user, setUser] = useState(null)
// Get authenticated user's id from state or wherever it's stored
const userId = user?.id;
useEffect(() => {
// Make sure this matches port used on backend
const ws = new ReconnectingWebSocket('ws://localhost:8080');
// Connect to websocket and pass userid to server
ws.onopen = () => {
console.log('Connected to WebSocket server')
ws.send(
JSON.stringify({
userId,
})
);
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log('Received data:', data);
//Get updated user details from backend.
//Confirm that user id of details sent matches userId of
//authenticated user before updating state
if (
data.type === 'status_update' &&
data.user &&
data.user._id === userId
) {
//Update user state
setUser(data.user);
}
} catch (error) {
console.error('Error parsing message:', error);
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
console.log('Disconnected from WebSocket server');
};
return () => {
ws.close();
};
}, []);
return (
<div>
{user}
</div>
)
}
export default Status;
Setting this up is pretty straightforward. The user ID is passed through the WebSocket to the server, which retrieves the user from the database and updates the status when necessary. The updated user details are then sent back to the frontend.
This approach reduces the number of HTTP requests between the client and server and allows for real-time updates without the client having to repeatedly ask for new information.
That's it! My two-way connection is set, and I couldn't be happier. This was my first time setting up and using WebSocket, and I loved it. I look forward to working on other projects with this technology and exploring other use cases that can be implemented with it.