How to

[Part 2] Real-time Auction System – Connecting Socket.io With React πŸ”₯

The previous article in this series introduced Socket.io, how to connect a React app to a Node.js server using Socket.io, and creating the user interface for the bidding system.

Nevo David
Nevo DavidAugust 9, 2022

Hi everybody, and welcome back!

A small recap

Like an actual auction, if you bid for a product, you get counterbids from other bidders. The auction runs on the β€œfast” decision bid, where somebody else will win or outbid you if you don’t bid fast enough.

To use online bidding, We must stick to the same principles. We must give our bidder information as soon as a new bid comes.

Bidding

To read the first part of the series, you can head over here:
[Part 1] Building a real-time Auction System with Socket.io and React.js

In this final article, I will guide you through sending notifications and messages between the client and the Node.js server.

Novu – the first open-source notification architecture

Just a quick background about us. Novu is the first open-source notification infrastructure. We basically help to manage all the product notifications. It can beΒ In-AppΒ (the bell icon like you have in Facebook – Websockets), Emails, SMSs, and so on.
I would be super happy if you could give us a star! And let me also know in the comments ❀️
https://github.com/novuhq/novu

Image description

We are back! We will continue where we last stopped

Creating the JSON β€œdatabase” file

As described in the previous article, the JSON file will serve as the database for the application. Although this is not a secure way of saving data, this is just a demo. We’ll read and update the JSON file.

Navigate into theΒ serverΒ folder and create the JSON file.

1cd server
2touch data.json

Add some products to the file by copying the code below – an array containing different products with their price, name, owner, and the last bidder.

1{
2  "products": [
3    {
4      "name": "Audi 250",
5      "price": "500000",
6      "owner": "admiralty20",
7      "last_bidder": "samson35"
8    },
9    {
10      "name": "Lamborghini S50",
11      "price": "200000",
12      "owner": "susaske40",
13      "last_bidder": "geraldt01"
14    },
15    {
16      "name": "Ferrari F560",
17      "price": "100000",
18      "owner": "samson35",
19      "last_bidder": "admiralty20"
20    }
21  ]
22},

Update theΒ index.jsΒ file to render theΒ data.jsonΒ file. The code snippet below reads theΒ data.jsonΒ file and returns the JSON file atΒ http://localhost:4000/api, making it easy for the web browser to fetch and display to users.

1const express = require('express');
2const app = express();
3const PORT = 4000;
4const fs = require('fs');
5const http = require('http').Server(app);
6const cors = require('cors');
7const socketIO = require('socket.io')(http, {
8  cors: {
9    origin: 'http://localhost:3000',
10  },
11});
12
13//Gets the JSON file and parse the file into JavaScript object
14const rawData = fs.readFileSync('data.json');
15const productData = JSON.parse(rawData);
16
17app.use(cors());
18
19socketIO.on('connection', (socket) => {
20  console.log(`⚑: ${socket.id} user just connected!`);
21  socket.on('disconnect', () => {
22    console.log('πŸ”₯: A user disconnected');
23  });
24});
25
26//Returns the JSON file
27app.get('/api', (req, res) => {
28  res.json(productData);
29});
30
31http.listen(PORT, () => {
32  console.log(`Server listening on ${PORT}`);
33});

Next, update theΒ ProductsΒ page from the client folder to fetch the products from the JSON file and display its contents.

1import React, { useEffect, useState } from 'react';
2import { Link } from 'react-router-dom';
3import { useNavigate } from 'react-router-dom';
4
5const Products = () => {
6  const [products, setProducts] = useState(null);
7  const [loading, setLoading] = useState(true);
8  const navigate = useNavigate();
9
10  const handleBidBtn = (product) =>
11    navigate(`/products/bid/${product.name}/${product.price}`);
12
13  useEffect(() => {
14    const fetchProducts = () => {
15      fetch('http://localhost:4000/api')
16        .then((res) => res.json())
17        .then((data) => {
18          setProducts(data.products);
19          setLoading(false);
20        });
21    };
22    fetchProducts();
23  }, []);
24
25  return (
26    <div>
27      <div className="table__container">
28        <Link to="/products/add" className="products__cta">
29          ADD PRODUCTS
30        </Link>
31
32        <table>
33          <thead>
34            <tr>
35              <th>Name</th>
36              <th>Price</th>
37              <th>Last Bidder</th>
38              <th>Creator</th>
39              <th>Edit</th>
40            </tr>
41          </thead>
42          <tbody>
43            {loading ? (
44              <tr>
45                <td>Loading</td>
46              </tr>
47            ) : (
48              products.map((product) => (
49                <tr key={`${product.name}${product.price}`}>
50                  <td>{product.name}</td>
51                  <td>{product.price}</td>
52                  <td>{product.last_bidder || 'None'}</td>
53                  <td>{product.owner}</td>
54                  <td>
55                    <button onClick={() => handleBidBtn(product)}>Edit</button>
56                  </td>
57                </tr>
58              ))
59            )}
60          </tbody>
61        </table>
62      </div>
63    </div>
64  );
65};
66
67export default Products;

From the code snippet above, theΒ ProductsΒ components fetch the products from the server and render them in a table.
Within the table, the Edit button has a click event listener that accepts the data related to each product and navigates to the bid page using the product’s name and price.

Next, let’s learn how to add products via the form in the React app to the Node.js server.

Adding products to the JSON file

We have a call to action in theΒ ProductsΒ component that links to theΒ AddProductΒ page, where the user provides the name and price of the product available for bidding. The username is retrieved from the local storage.

Add Product page

1import React, { useState } from 'react';
2import { useNavigate } from 'react-router-dom';
3
4const AddProduct = () => {
5  const [name, setName] = useState('');
6  const [price, setPrice] = useState(0);
7  const navigate = useNavigate();
8
9  const handleSubmit = (e) => {
10    e.preventDefault();
11    console.log({ name, price, owner: localStorage.getItem('userName') });
12    navigate('/products');
13  };
14
15  return (
16    <div>
17      <div className="addproduct__container">
18        <h2>Add a new product</h2>
19        <form className="addProduct__form" onSubmit={handleSubmit}>
20          <label htmlFor="name">Name of the product</label>
21          <input
22            type="text"
23            name="name"
24            value={name}
25            onChange={(e) => setName(e.target.value)}
26            required
27          />
28
29          <label htmlFor="price">Starting price</label>
30          <input
31            type="number"
32            name="price"
33            value={price}
34            onChange={(e) => setPrice(e.target.value)}
35            required
36          />
37
38          <button className="addProduct__cta">SEND</button>
39        </form>
40      </div>
41    </div>
42  );
43};
44
45export default AddProduct;

Next, we will send the product data to the Node.js server for storage via Socket.io. We passed Socket.io as a prop into every component from theΒ src/App.jsΒ file.
Destructure Socket.io from the props object and update theΒ handleSubmitΒ function as below:

1const AddProduct = ({ socket }) => {
2  const [name, setName] = useState('');
3  const [price, setPrice] = useState(0);
4  const navigate = useNavigate();
5
6  const handleSubmit = (e) => {
7    e.preventDefault();
8    // console.log({ name, price, owner: localStorage.getItem('userName') });
9    socket.emit('addProduct', {
10      name,
11      price,
12      owner: localStorage.getItem('userName'),
13    });
14    navigate('/products');
15  };
16
17  return <div>...</div>;
18};
19export default AddProduct;

From the code snippet above, theΒ addProductΒ event sends an object containing the product’s name, price, and owner to the Node.js server via Socket.io.

Create an event on the Node.js server that listens to theΒ addProductΒ message from the client.

1/*
2The other lines of code
3*/
4const rawData = fs.readFileSync('data.json');
5const productData = JSON.parse(rawData);
6
7socketIO.on('connection', (socket) => {
8  console.log(`⚑: ${socket.id} user just connected!`);
9  socket.on('disconnect', () => {
10    console.log('πŸ”₯: A user disconnected');
11  });
12
13  //Listens to the addProduct event
14  socket.on('addProduct', (data) => {
15    console.log(data); //logs the message from the client
16  });
17});
18// ....<The other lines of code>

Server data

Since we’ve been able to access the data sent from the client, let’s save the data to the database file.

1/*
2The other lines of code
3*/
4const rawData = fs.readFileSync('data.json');
5const productData = JSON.parse(rawData);
6
7socketIO.on('connection', (socket) => {
8  console.log(`⚑: ${socket.id} user just connected!`);
9  socket.on('disconnect', () => {
10    console.log('πŸ”₯: A user disconnected');
11  });
12  socket.on('addProduct', (data) => {
13    productData['products'].push(data);
14    const stringData = JSON.stringify(productData, null, 2);
15    fs.writeFile('data.json', stringData, (err) => {
16      console.error(err);
17    });
18  });
19});
20// ....<The other lines of code>

TheΒ addProductΒ event listens to messages from the client and updates theΒ data.jsonΒ file by adding the product data to the products array and saving it to theΒ data.jsonΒ file.

Congratulations, we’ve been able to read and save data to our JSON database. Next, let’s learn how to update product prices when users bid for items.

Updating the JSON file

In this section, we’ll enable users to update the price of the items in the JSON file. The changes will also be persistent even after refreshing the page.

Since theΒ BidProductΒ page accepts the product’s data via URL parameters, we’ll need to use theΒ useParamsΒ hook provided byΒ React Router.

1import React, { useState } from 'react';
2import { useNavigate } from 'react-router-dom';
3import { useParams } from 'react-router-dom';
4
5const BidProduct = () => {
6  //sets the default value as the current price from the Product page
7  const [userInput, setUserInput] = useState(price);
8
9  //Destructured from the URL
10  const { name, price } = useParams();
11  const navigate = useNavigate();
12
13  const handleSubmit = (e) => {
14    e.preventDefault();
15    navigate('/products');
16  };
17
18  return <div>...</div>;
19};

TheΒ bidProductΒ URL contains the name and price of the selected product from theΒ ProductsΒ page. TheΒ useParamsΒ hook enables us to destructure the name and price of the product from the URL. Then, we can set the default value of the input field (bid) to the current price from theΒ ProductsΒ page.

Update theΒ BidProduct.jsΒ component above by adding the Socket.io prop fromΒ src/App.jsΒ to enable us to send the new bid to the Node.js server.

1import React, { useState } from 'react';
2import { useNavigate } from 'react-router-dom';
3import { useParams } from 'react-router-dom';
4
5const BidProduct = ({ socket }) => {
6  const { name, price } = useParams();
7  const [userInput, setUserInput] = useState(price);
8  const navigate = useNavigate();
9  const [error, setError] = useState(false);
10
11  const handleSubmit = (e) => {
12    e.preventDefault();
13    if (userInput > Number(price)) {
14      socket.emit('bidProduct', {
15        userInput,
16        last_bidder: localStorage.getItem('userName'),
17        name,
18      });
19      navigate('/products');
20    } else {
21      setError(true);
22    }
23  };
24
25  return (
26    <div>
27      <div className="bidproduct__container">
28        <h2>Place a Bid</h2>
29        <form className="bidProduct__form" onSubmit={handleSubmit}>
30          <h3 className="bidProduct__name">{name}</h3>
31
32          <label htmlFor="amount">Bidding Amount</label>
33          {/* The error message */}
34          {error && (
35            <p style={{ color: 'red' }}>
36              The bidding amount must be greater than {price}
37            </p>
38          )}
39
40          <input
41            type="number"
42            name="amount"
43            value={userInput}
44            onChange={(e) => setUserInput(e.target.value)}
45            required
46          />
47
48          <button className="bidProduct__cta">SEND</button>
49        </form>
50      </div>
51    </div>
52  );
53};
54
55export default BidProduct;

From the code snippet above, theΒ handleSubmitΒ function checks if the new value provided by the user is greater than the default price. If so, it triggers theΒ bidProductΒ event that sends an object containing the user input (new price), the name of the product, and the last bidder to the Node.js server. Otherwise, React displays an error message to the user.

Next, let’s create theΒ bidProductΒ event listener on the server to accept the data sent from the client. Update the Socket.io code block in the index.js file on the server as below:

1socketIO.on('connection', (socket) => {
2  console.log(`⚑: ${socket.id} user just connected!`);
3  socket.on('disconnect', () => {
4    console.log('πŸ”₯: A user disconnected');
5  });
6
7  socket.on('addProduct', (data) => {
8    productData['products'].push(data);
9    const stringData = JSON.stringify(productData, null, 2);
10    fs.writeFile('data.json', stringData, (err) => {
11      console.error(err);
12    });
13  });
14
15  //Listens for new bids from the client
16  socket.on('bidProduct', (data) => {
17    console.log(data);
18  });
19});

Update the price of the selected product and save it in theΒ data.jsonΒ file by copying the function below:

1function findProduct(nameKey, productsArray, last_bidder, new_price) {
2  for (let i = 0; i < productsArray.length; i++) {
3    if (productsArray[i].name === nameKey) {
4      productsArray[i].last_bidder = last_bidder;
5      productsArray[i].price = new_price;
6    }
7  }
8  const stringData = JSON.stringify(productData, null, 2);
9  fs.writeFile('data.json', stringData, (err) => {
10    console.error(err);
11  });
12}

The function accepts the list of products, name, last bidder, and the new price of the product then loops through every object in the array until it finds a matching product name. Then, it updates the last bidder and price of the product in theΒ data.jsonΒ file.

Call the function within the Socket.io code to set the price and last bidder of the selected product.

1....
2....
3  socket.on('bidProduct', (data) => {
4    //Function call
5    findProduct(
6      data.name,
7      productData['products'],
8      data.last_bidder,
9      data.amount
10    );
11  });
12});

Congratulations, users can now bid for items on the web application. Next, we will learn how to notify users when an item is added or put up for action.

Sending notifications to users via Socket.io

In this section, we’ll connect the Nav component to the Node.js server, so whenever a user adds a product and places a bid, the server sends a message to the React app.

Update the Socket.io code block in theΒ index.jsΒ file as below:

1socketIO.on('connection', (socket) => {
2  console.log(`⚑: ${socket.id} user just connected!`);
3  socket.on('disconnect', () => {
4    console.log('πŸ”₯: A user disconnected');
5  });
6
7  socket.on('addProduct', (data) => {
8    productData['products'].push(data);
9    const stringData = JSON.stringify(productData, null, 2);
10    fs.writeFile('data.json', stringData, (err) => {
11      console.error(err);
12    });
13
14    //Sends back the data after adding a new product
15    socket.broadcast.emit('addProductResponse', data);
16  });
17
18  socket.on('bidProduct', (data) => {
19    findProduct(
20      data.name,
21      productData['products'],
22      data.last_bidder,
23      data.amount
24    );
25
26    //Sends back the data after placing a bid
27    socket.broadcast.emit('bidProductResponse', data);
28  });
29});

Socket.io sends a response to the React app whenever a user performs one of the actions.
Now, you can create an event listener on the client and display the data as a notification.

1import React, { useState, useEffect } from 'react';
2
3const Nav = ({ socket }) => {
4  const [notification, setNotification] = useState('');
5
6  //Listens after a product is added
7  useEffect(() => {
8    socket.on('addProductResponse', (data) => {
9      setNotification(
10        `@${data.owner} just added ${data.name} worth $${Number(
11          data.price
12        ).toLocaleString()}`
13      );
14    });
15  }, [socket]);
16
17  //Listens after a user places a bid
18  useEffect(() => {
19    socket.on('bidProductResponse', (data) => {
20      setNotification(
21        `@${data.last_bidder} just bid ${data.name} for $${Number(
22          data.amount
23        ).toLocaleString()}`
24      );
25    });
26  }, [socket]);
27
28  return (
29    <nav className="navbar">
30      <div className="header">
31        <h2>Bid Items</h2>
32      </div>
33
34      <div>
35        <p style={{ color: 'red' }}>{notification}</p>
36      </div>
37    </nav>
38  );
39};
40
41export default Nav;

Congratulations on making it this far!πŸ’ƒπŸ»

Conclusion

Socket.io is a great tool with excellent features that enables us to build various real-time applications like chat apps, forex trading applications, and many others. Socket.io creates lasting connections between web browsers and a Node.js server.

This project is a demo of what you can build with Socket.io; you can improve this application by adding authentication and creating categories for the products.

The complete code for this tutorial isΒ available on GitHub.

Help me out!

If you feel like this article helped you understand WebSockets better! I would be super happy if you could give us a star! And let me also know in the comments ❀️
https://github.com/novuhq/novu

Image description

Thank you for reading! πŸš€

Nevo David
Nevo DavidAugust 9, 2022

Related Posts

How to

How To Add In-App Notifications To Your Angular App

How to add an in-app notification center to your Angular app

Emil Pearce
Emil PearceMay 2, 2023
How to

Make a Dream Todo app with Novu, React and Express!

In this article, you'll learn how to make a todo app that uses Novu to work for you. The prime focus in making this app was to boost personal productivity, get things done and stay distraction free!

Sumit Saurabh
Sumit SaurabhApril 19, 2023
How to

Building a forum with React, NodeJS

In this article, you'll learn how to build a forum system that allows users to create, react, and reply to post threads. In the end, we will also send a notification on each reply on a thread with Novu, you can skip the last step if you want only the technical stuff.

Nevo David
Nevo DavidFebruary 27, 2023