Creating a Real Time Chat Application with React, Node, and TailwindCSS

In this tutorial, we will show you how to build a real-time chat application using React and Vite,as well as a simple Node backend. We will use SocketIO for real-time communication and Tailwind CSS for styling.

Real Time Chat with React

What you're going to build.

(Full source code down below!)

Setting Up the Project

First, let’s create a new folder, real-time-chat. In this folder, we’ll setup a React project using Vite.

npm create vite@latest client
cd client
npm install

Note: Go through the installation by selecting “React” and then “JavaScript”.

Installing Dependencies

For this project, we’ll be using various libraries, including socket.io and tailwindCSS. Run the following in the client folder you just set up:

npm install socket.io-client
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Configuring Tailwind CSS

Next, lets configure Tailwind CSS. The last command you ran above should have generated a tailwind.config.js file. Include the following:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};

Next, we’ll want to edit the src/index.css file, replacing everything with the following:

@tailwind base;
@tailwind components;
@tailwind utilities;

Now the project is ready to use Tailwind.

You can also delete App.css, and the /assets folder, as we won’t be using them. However, you’ll need to remember to go to App.jsx and remove those imports.

Setting Up the Server

We need a simple server to handle the WebSocket connections. Let’s set up an Express server with SocketIO.

Create a new directory in the real-time-chat folder. We’ll call it server.

mkdir server
cd server
npm init -y
npm install express socket.io

Next, create an index.js file in the server directory and add the following code:

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

const app = express();
const server = http.createServer(app);
const io = new Server(server, {
  cors: {
    origin: "http://localhost:5173",
    methods: ["GET", "POST"],
  },
});

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

  socket.on("chat message", (msg) => {
    io.emit("chat message", msg);
  });

  socket.on("disconnect", () => {
    console.log("user disconnected");
  });
});

server.listen(3000, () => {
  console.log("listening on *:3000");
});

Start the server by typing node index.js in the terminal.

Note: Restarting the server every time you make a change can get annoying quickly. If you’d like, you can install nodemon by typing npm i --save-dev nodemon. Now, in package.json, you can add a “start”, and “dev” command under “scripts”, like so:

Real Time Chat with React

Now you can type npm run dev, and as you make changes in index.js, the server will automatically restart for you. Although, for this article, we are finished with the server – this is just a tip if you plan on extending the functionality of this application.

Building the Frontend

Now, let’s build the frontend of our chat application, starting with the emoji picker, of which we’ll be importing.

Let’s install the emoji-picker-react library:

npm i emoji-picker-react

Now we’re free to use it:

/* A blank page with an emoji selector */
import Picker from "emoji-picker-react";

function App() {
  return (
    <div>
      <Picker />
    </div>
  );
}

export default App;

Although, that’s just an example, and not at all what our App.jsx is going to look like. In fact, we don’t want the emoji to be displayed like that at all times, but a user should be able to open and close it.

So, in src, let’s create a component, EmojiPicker.jsx. In this file, add the following:

import React, { useState, useRef, useEffect } from "react";
import Picker from "emoji-picker-react";

const EmojiPicker = ({ onEmojiClick }) => {
  const [showPicker, setShowPicker] = useState(false);
  const pickerRef = useRef(null);

  const togglePicker = () => {
    setShowPicker(!showPicker);
  };

  const handleClickOutside = (event) => {
    if (pickerRef.current && !pickerRef.current.contains(event.target)) {
      setShowPicker(false);
    }
  };

  useEffect(() => {
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, []);

  return (
    <div className="relative" ref={pickerRef}>
      <button
        type="button"
        className="p-2 bg-gray-200 rounded-full hover:bg-gray-300 focus:outline-none"
        onClick={togglePicker}
      >
        😀
      </button>
      {showPicker && (
        <div className="absolute bottom-12 right-0 z-10">
          <Picker onEmojiClick={onEmojiClick} />
        </div>
      )}
    </div>
  );
};

export default EmojiPicker;

Setting up the Chat components

Our application will essentially contain three components: ChatBox.jsx, ChatMessages.jsx, and ChatInput.jsx.

Looking at the component names, you might be able to get an idea of what each of them does. ChatBox.jsx will contain everything, it is the parent component. ChatMessages.jsx will contain the logic for displaying the messages that will be handles with ChatInput.jsx. Create these three files in the src folder.

So, let’s start by defining ChatBox.jsx:

import React from "react";
import { ChatMessages } from "./ChatMessages";
import { ChatInput } from "./ChatInput";

const ChatBox = () => {
  return (
    <>
      <ChatMessages />
      <ChatInput />
    </>
  );
};

export default ChatBox;

Currently, these two components don’t contain any code, so you’ll surely see some errors coming from Vite.

No worries, paste the following code into ChatMessages.jsx:

import React, { useEffect, useState, useRef } from "react";
import { io } from "socket.io-client";

const socket = io("http://localhost:3000");

export const ChatMessages = () => {
  const [messages, setMessages] = useState([]);
  const messagesEndRef = useRef(null);

  useEffect(() => {
    socket.on("chat message", (msg) => {
      setMessages((prevMessages) => [...prevMessages, msg]);
    });

    return () => {
      socket.off("chat message");
    };
  }, []);

  useEffect(() => {
    if (messagesEndRef.current) {
      messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
    }
  }, [messages]);

  return (
    <div className="h-64 overflow-y-auto mb-4 p-4 bg-gray-100 rounded-lg">
      {messages.map((msg, index) => (
        <div
          key={index}
          className={`p-2 mb-2 rounded-lg text-left ${
            index % 2 === 0 ? "bg-blue-100" : "bg-indigo-300"
          }`}
        >
          {msg}
        </div>
      ))}
      <div ref={messagesEndRef} />
    </div>
  );
};

There is quite a lot going on in this component, and we can probably make another component out of this one, but for the sake of this article, we’re just going to roll with it. The first useEffect hook in the code above sets up a side effect in the component that listens for incoming chat messages from the WebSocket connection, updating the component’s state, defined with const [messages, setMessages] = useState([]), with the new messages.

The second useEffect hook essentially just scrolls to the current message, based on the useRef() hook. The code within return () now just essentially displays the messages sent.

At this point, if you were to remove the call to <ChatInput /> from the ChatBox component, you should see something like this:

ChatBox component

It’s completely ready to accept some messages and display them, but now we need to build the ChatInput compnent.

import React, { useState } from "react";
import { io } from "socket.io-client";
import EmojiPicker from "./EmojiPicker";

const socket = io("http://localhost:3000");

export const ChatInput = () => {
  const [message, setMessage] = useState("");

  const sendMessage = (e) => {
    e.preventDefault();
    if (message.trim()) {
      socket.emit("chat message", message);
      setMessage("");
    }
  };

  const handleEmojiClick = (emoji, event) => {
    setMessage((prevMessage) => prevMessage + emoji.emoji);
  };

  return (
    <form onSubmit={sendMessage} className="flex items-center">
      <div className="m-2">
        <EmojiPicker onEmojiClick={handleEmojiClick} />
      </div>
      <input
        type="text"
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        className="flex-grow p-2 border border-gray-300 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
        placeholder="Type your message..."
      />
      <button
        type="submit"
        className="p-2 bg-indigo-500 text-white rounded-r-lg hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500"
      >
        Send
      </button>
    </form>
  );
};

This component handles the submission of messages, as well as contains the <EmojiPicker /> component, which is a nice addition to the web based chat application.

The last step is to import the Chat.jsx component into App.jsx,

import React from "react";
import Chat from "./Chat";

function App() {
  return (
    <div className="flex flex-col items-center justify-center h-screen">
      <div className="w-full max-w-md p-4 bg-white shadow-lg rounded-lg">
        <Chat />
      </div>
    </div>
  );
}

export default App;

By now, you should have a functioning chat application, of which you can test by opening open another browser tab, and sending messages between the two.

Source Code

Conclusion

You now have a pretty neat real-time chat application, eh?

But… it can be better, right? Like, what if you wanted to unsend or edit a message? The styling can probably be improved a bit too. You can create a full-page chat application, or keep this as a mini component for a larger application. The choice is ultimately up to you!

Although, go ahead and practice your React skills by updating this mini application to have features you’d like to see. Share your results in the comments below! :)

comments powered by Disqus

Related Posts

The Importance of Staying Active as a Software Developer

In today’s fast-paced digital world, developers often find themselves glued to their screens for extended periods. While this dedication is commendable, it comes with its own set of challenges.

Read more

JavaScript DOM Mastery: Top Interview Questions Explained

Mastering the Document Object Model (DOM) is crucial for any JavaScript developer. While many developers rely heavily on front-end frameworks, the underlying DOM concepts are still important.

Read more

Solving a Common Interview Question: the Two Sum Algorithm in JavaScript

Imagine you’re at a lively party, and everyone is carrying a specific number on their back. The host announces a game – find two people whose numbers add up to the magic number, and you win a prize!

Read more