Forem

Cover image for Mastering WebSockets with Socket.IO: A Comprehensive Guide
Hesbon limo
Hesbon limo

Posted on

Mastering WebSockets with Socket.IO: A Comprehensive Guide

What is a WebSocket?

WebSocket is a communication protocol that enables two-way communication between a server and a client over a long-lived connection. A protocol is a set of rules and conventions that dictate how data is being transmitted between two or more computer devices.

Basically websockets are a set of ideas about one particular way where we work with data. Unlike traditional APIs and HTTP connections, websockets allow for real-time communication making it ideal for chart applications and sports betting sites.

The major difference between HTTP and WebSockets is how the client and the server communicate. HTTP requests send a request to the server then the server sends a response to the client and then the connection ends.

On the other hand, WebSockets sends a handshake request to the server and if the server is capable of supporting the websocket protocol it will send back a confirmation. Once the connection is established both parties can send and receive information without waiting for the request-response cycle.

Even though web sockets are long-lived they include mechanisms for detecting and handling dropped connections and inactivity. For example, you could specify that if neither the server nor the client has sent a message in the last ten minutes the connection should be stopped.

Picture this in an online chatting app. Two users are able to chat in real time with the application showing whether the other party is online, typing or has deleted a message. That is the real-world application of WebSockets.

Building a CRUD Operation using socket.io

We are going to build a CRUD application where you can create, read, update, and delete text using socket.io.

Setting up the environment

Prerequisites

Make sure node.js has been installed in your environment. If not, run the following command in your terminal globally:

sudo apt install -y nodejs
Enter fullscreen mode Exit fullscreen mode

After installation, run the following command to check the version of node js installed:

node -v
Enter fullscreen mode Exit fullscreen mode

Setting up the backend

In your code editor, create a folder named server. After creating the folder, run the following command in your terminal, to install the necessary node modules, run this command:

npm install
Enter fullscreen mode Exit fullscreen mode

To install the socket.io package in your backend folder, run this command:

npm install socket.io
Enter fullscreen mode Exit fullscreen mode

Setting up the frontend

In your code editor, create a folder named client. After creating the client folder, run npm install to install the necessary node modules. After installing the node modules, it's time to install the socket.io package in your frontend folder using the following command:

npm install socket.io
Enter fullscreen mode Exit fullscreen mode

Implementing the frontend

In the client folder you had created earlier, create a React app using vite by running the following command:

npm create vite@latest 
Enter fullscreen mode Exit fullscreen mode

After creating the React app we are going to write our code in the app.jsx. Here is the code:

import { useEffect, useState } from "react";
import { io } from "socket.io-client";
import "./App.css";
import { v4 as uuidv4 } from "uuid";

function App() {
  const [formInputs, setFormInputs] = useState({});
  const [crudData, setCrudData] = useState([]); // Initialize as an empty array
  const [isEdit, setIsEdit] = useState(false);
  const socket = io("http://localhost:3000");

  const handleInput = (event) => {
    const { name, value } = event.target;
    setFormInputs((prev) => ({ ...prev, [name]: value }));
  };

  const handleSubmit = () => {
    if (!formInputs.name || !formInputs.age || !formInputs.number) {
      console.error("All fields must be filled before submitting.");
      return;
    }

    const newData = {
      ...formInputs,
      id: uuidv4(), // Generate a unique ID for the new data
    };

    socket.emit("data", newData); // Send the new data to the server
    setFormInputs({}); // Clear the input fields after submission
  };

  const handleEdit = () => {
    if (!formInputs.id) {
      console.error("No item selected for editing.");
      return;
    }

    console.log("Submitting edited data:", formInputs);
    socket.emit("editData", formInputs); // Emit the edited data to the server
    setIsEdit(false); // Reset edit mode
    setFormInputs({}); // Clear the form inputs
  };

  const handleDelete = (id) => {
    console.log("Deleting item with ID:", id);
    socket.emit("deleteData", id); // Emit the delete request to the server
  };

  useEffect(() => {
    // Listen for updated CRUD data from the server
    socket.on("crudData", (response) => {
      console.log("Updated data received from server:", response);
      if (Array.isArray(response)) {
        setCrudData(response); // Update the UI with the latest data
      } else {
        console.error("Unexpected response format:", response);
      }
    });

    return () => {
      socket.off("crudData"); // Clean up listener
    };
  }, []);

  const getEditData = (data) => {
    setFormInputs(data);
    setIsEdit(true);
  };

  return (
    <>
      <h1>CRUD Operations</h1>
      <div className="form-fields">
        <input
          onChange={handleInput}
          className="input-field"
          name="name"
          placeholder="Enter your name"
          value={formInputs.name || ""}
        />
        <input
          onChange={handleInput}
          className="input-field"
          name="age"
          placeholder="Enter your age"
          value={formInputs.age || ""}
        />
        <input
          onChange={handleInput}
          className="input-field"
          name="number"
          placeholder="Enter your phone number"
          value={formInputs.number || ""}
        />
      </div>
      <button onClick={isEdit ? handleEdit : handleSubmit}>
        {isEdit ? "Edit" : "Add"} Data
      </button>
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Age</th>
            <th>Phone Number</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          {Array.isArray(crudData) && crudData.length > 0 ? (
            crudData.map((data, index) => (
              <tr key={index}>
                <td>{data.name}</td>
                <td>{data.age}</td>
                <td>{data.number}</td>
                <td>
                  <button onClick={() => getEditData(data)}>Edit</button>
                  <button onClick={() => handleDelete(data.id)}>Delete</button>
                </td>
              </tr>
            ))
          ) : (
            <tr>
              <td colSpan="4">No data available</td>
            </tr>
          )}
        </tbody>
      </table>
    </>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Let's discuss this frontend code in detail. First, we start with the imports in lines 1 to 4:

  • useState and useEffect: These are React hooks for managing state and side effects.
  • io: This is simply a socket.io package used for real-time communication to the server.
  • uuidv4: This is used to create a unique ID for each input in the application.

Now let's move to the initialization from lines 7 to 10:

  • formInput: This is for storing the form inputs from the user of the app that is name, age, and phone number.
  • crudData: This is used to store data sent to the server or received from the server.
  • isEdit: It's used to check if the button is in the submit or edit mode.

In line 10 we are connecting the frontend to the server which is running in local host port 3000.

The handleInput function in line 12 is used to update the formInputs state as the user types in the values.

The handleSubmit function is used to submit the new data to the server. It works by checking if all the fields have been entered, if they are, it will create a new ID for the data and send it to the server. Finally, it clears the input field after the data has been submitted.

After that, we move to the handleEdit function from line 32. The function works by checking if an input field has been selected for editing. If so, It will emit the edited data to the server. Then it resets the edit mode and clears the input field.

handleDelete function is used to delete data from the UI and also submit the delete request to the server using socket.io for real-time communication.

The useEffect hook is used to check for any updates to the crudData from the server then it updates it in our UI. By ui, I mean the frontend or what you see in the browser.

getEditData function enables us to edit our data by setting the isEdit state to true.

The return method from lines 70 to 130 is used to render the UI. It displays the input fields for name, age, and phone number and a button that switches between edit and add. It also displays all the data we have and buttons to edit and delete each entry.

Implementing the Backend

Now let's move to the backend and discuss the nodejs code.

In the server folder create a file and name it server.js.

const { createServer } = require("http");
const { Server } = require("socket.io");

// Create the HTTP server
const httpServer = createServer();
const io = new Server(httpServer, {
  cors: {
    origin: "http://localhost:5173", // Allow requests from the frontend
  },
});

let crudData = []; // Array to store all CRUD data

io.on("connection", (socket) => {
  console.log("A client connected");

  // Send the current data to the newly connected client
  socket.emit("crudData", crudData);

  // Handle receiving new data from the client
  socket.on("data", (data) => {
    console.log("Received new data:", data);

    // Add the new data to the array
    crudData.push(data);

    // Broadcast the updated data to all connected clients
    io.emit("crudData", crudData);
  });

  // Handle editing data
  socket.on("editData", (response) => {
    console.log("Edit data received:", response);

    // Find the index of the item to be edited
    const currentIndex = crudData.findIndex((data) => data.id === response.id);

    if (currentIndex !== -1) {
      // Update the item
      crudData[currentIndex] = { ...crudData[currentIndex], ...response };

      // Broadcast the updated data to all clients
      io.emit("crudData", crudData);
      console.log("Updated data broadcasted:", crudData);
    } else {
      console.error("Item not found for editing");
    }
  });

  // Handle deleting data
  socket.on("deleteData", (id) => {
    console.log("Delete request received for ID:", id);

    // Filter out the item to delete
    crudData = crudData.filter((data) => data.id !== id);

    // Broadcast the updated data to all clients
    io.emit("crudData", crudData);
    console.log("Updated data after deletion:", crudData);
  });

  // Handle client disconnect
  socket.on("disconnect", () => {
    console.log("A client disconnected");
  });
});

// Start the server
httpServer.listen(3000, () => {
  console.log("Server is connected and listening on http://localhost:3000");
});
Enter fullscreen mode Exit fullscreen mode

In lines 1 and 2, we are declaring an HTTP server and initializing socket.io for real-time communication.

Lines 4 to 10 create an HTTP server associated with socket.io and then connect to the front end which is running in port 5173 using the CORS policy which prevents it from blogging the connection. In line 12 we are declaring an empty array to store all the inputs from the frontend.

The io.on event in line 14 is an event that connects to the client and emits all the data in the server to the newly connected client. Socket.on the event in line 21 handles new data from the client and publishes the data in the crudData array using the push array method. It then broadcast the newly added data to the client using io.emmit.

In line 32 we have the socket.io editaData event which handles data editing in the server. When the user clicks edit in the client, the server searches for the data using the findIndex method. If it exists it updates the data in the crudData array then it broadcasts the edited data to the client.

The socket.io deleteData event in line 52 handles the deleting of data in the server. When the client initiates a delete function, the server finds the data using its ID and deletes it from crudData array using the filter method. The event then broadcasts the updated data to the client.

Testing

I am going to show you how real-time communication works with web sockets by opening two tabs and also show the server side.

As you can see from the video, initially, I had two entries in my applications then I added the third one and it immediately updated. Then I switched to another tab and there were also three entries same as the previous tab. I also deleted one entry and it was updated in both tabs.

When I switch to the server you can see the updated data after deletion. I then edited the name of the first entry from kiprop rono to kiprop kiptoo and it updated both in the server and the ui.

Conclusion

Socket.io is a very powerful tool that is used by various chat applications and betting apps for their real-time communication.

In this article, we have discussed what are WebSockets, WebSockets vs HTTP protocol, and its common application. We have also built a real-time CRUD application using react, socket.io, and node Js.

For more information, check out my GitHub link here.

Top comments (0)