Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Go

Building a Real-Time Chat Application with Go and React

5.00/5 (9 votes)
8 Oct 2024CPOL6 min read 7.2K   119  
A real-time chat application built with a Go WebSocket backend and a React frontend, enabling instant messaging between users.
This project is a real-time chat application utilizing a Go backend with WebSocket technology for handling communication and a React frontend for the user interface. The Go server efficiently manages WebSocket connections, enabling instant message broadcasting between connected clients. The React frontend offers a responsive chat interface, allowing users to send and receive messages in real-time. Together, these technologies create a scalable and high-performance chat system, demonstrating how modern web applications can leverage WebSocket for seamless real-time communication.

Image 1

Introduction

In this article, we will walk through the development of a real-time chat room application using Go for the backend (WebSocket server) and React for the frontend. This project highlights how modern web applications can use WebSocket for instant, real-time communication.

Project Overview

The chat application allows users to connect and send messages in real-time. When a user sends a message, it is broadcast to all connected clients without the need to refresh the page. We use Go for handling WebSocket connections on the server side, and React for rendering the UI on the client side.

Project Structure

The project consists of two parts:

  1. Go backend: A WebSocket server that handles real-time communication.
  2. React frontend: A web-based user interface for sending and receiving messages

Here is an overview of the project structure:

├── real-time-chat/    # Go WebSocket server (backend)
├── chatroom/          # React chat application (frontend)
└── README.md          # Project documentation

The Go WebSocket Server (Backend)

Why Go?

Go (or Golang) is a great language for building high-performance network applications due to its concurrency model and efficient handling of I/O operations. For this project, Go's net/http package and gorilla/websocket package provide an efficient way to handle WebSocket connections, ensuring messages are exchanged in real time.

Prerequisites

1. Install VS Code

If haven't already, download and install VS Code from the official website.

2. Install Go

Make sure Go is installed on your system. Download it from the Go website.

After installation, confirm Go is set up correctly by running:

Shell
go version

Ensure $GOPATH and $GOROOT are properly configured in environment variables.

3. Install the Go Extension for VS Code

  • Open VS Code.
  • Go to the Extensions panel (on the left sidebar or press Ctrl+Shift+X).
  • Search for Go (by the Go team at Google) and install it.

This extension will provide linting, auto-formatting, IntelliSense, and other development tools.

4. Set Up Go Tools

Once the Go extension is installed, VS Code will prompt you to install some additional Go tools (like gopls, gofmt, delve for debugging, etc.). These tools enhance your development experience.

When prompted to install tools, click Install All, or can install them manually by running:

go install golang.org/x/tools/gopls@latest
go install golang.org/x/lint/golint@latest
go install github.com/go-delve/delve/cmd/dlv@latest
go install golang.org/x/tools/cmd/goimports@latest

5. Go Modules Support

If using Go modules (for dependency management), make sure you're in a Go module project. Initialize a new module with:

Shell
go mod init project-name

Setting Up the Backend

Step 1: Create the Go WebSocket Server

We start by creating the Go backend, which listens for WebSocket connections, manages active clients, and broadcasts messages between them.

Here’s a simplified breakdown of the key components of the Go backend:

  1. WebSocket Connections: The server establishes WebSocket connections with clients.
  2. Client Management: The server keeps track of connected clients using a map.
  3. Broadcasting Messages: When one client sends a message, the server broadcasts it to all other clients in real time.

Step 2: Implementing the Go WebSocket Server

  • Create a Project Directory

First, create a folder for chat application.

Shell
mkdir real-time-chat
cd real-time-chat
  • Initialize a Go Module

To make use of Go modules for dependency management, initialize project with go mod:

Shell
go mod init real-time-chat
  • Organize Project Structure
real-time-chat/
│
├── go.mod               # For dependency management
├── main.go              # Entry point of your app
├── handlers/
│   └── websocket.go     # WebSocket-related logic
└── public/
    └── index.html       # Frontend (optional: for testing)
  • Create main.go for the Application Entry Point

main.go file will serve as the entry point to the application. This file will set up the server and handle incoming connections.

Here’s main.go file:

TypeScript
package main

import (
    "log"
    "net/http"
    "real-time-chat/handlers"
)

func main() {
    fs := http.FileServer(http.Dir("./public"))
    http.Handle("/", fs)
    
    http.HandleFunc("/ws", handlers.HandleConnections)
    
    log.Println("Server started on :8080")
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

Declares the package name as main, meaning this is the entry point of the application. Every Go application that run starts with the main package.

We import:

  1. log: Used to log server events (e.g., starting the server, errors).
  2. net/http: Provides HTTP functionalities to create a web server and handle requests.
  3. real-time-chat/handlers: A custom package where the WebSocket connection logic is defined. This is the file we discussed earlier (handlers/websocket.go).
  4. http.FileServer: Serves static files (like HTML, CSS, and JS). Here, it's serving files from the public/ directory.
  5. http.Handle("/", fs): Routes all requests to the root URL (/) to the file server, so when users visit http://localhost:8080, they will see the static index.html file from the public/ folder.

 

  • Create a handlers/websocket.go File for WebSocket Logic

Separate the WebSocket logic into a new websocket.go file under a handlers/ folder. This will keep code more modular and organized.

websocket.go file:

TypeScript
package handlers

import (
    "log"
    "net/http"

    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool { return true },
}

var clients = make(map[*websocket.Conn]bool) // Connected clients
var broadcast = make(chan Message)           // Channel for broadcasting messages

// Message defines the structure of the messages exchanged
type Message struct {
    Username  string `json:"username"`
    Message   string `json:"message"`
    Timestamp string `json:"timestamp"`
    Typing    bool   `json:"typing"` // Indicates if the user is typing
}

// HandleConnections handles new WebSocket requests from clients
func HandleConnections(w http.ResponseWriter, r *http.Request) {
    ws, err := upgrader.Upgrade(w, r, nil) // Upgrade HTTP to WebSocket
    if err != nil {
        log.Fatal(err)
    }
    defer ws.Close() // Close the WebSocket connection when the function returns

    clients[ws] = true // Register the new client

    for {
        var msg Message
        // Read a new message as JSON and map it to a Message object
        err := ws.ReadJSON(&msg)
        if err != nil {
            log.Printf("error: %v", err)
            delete(clients, ws) // Remove the client from the list if there is an error
            break
        }
        // Send the message to the broadcast channel
        broadcast <- msg
    }
}

// HandleMessages broadcasts incoming messages to all clients
func HandleMessages() {
    log.Println("HandleMessages running")
    for {
        // Get the next message from the broadcast channel
        msg := <-broadcast
        // Send it to every connected client
        for client := range clients {
            err := client.WriteJSON(msg) // Write message to the client
            if err != nil {
                log.Printf("error: %v", err)
                client.Close()          // Close the connection if there's an error
                delete(clients, client) // Remove the client
            }
        }
    }
}
  1. http.HandleFunc: Registers a new route for the WebSocket connection at /ws. When a client connects to ws://localhost:8080/ws, this route handles it.
  2. handlers.HandleConnections: This function (defined in websocket.go) handles the WebSocket connection for each client. It upgrades the HTTP connection to a WebSocket connection.
  3. log.Println("Server started on :8080"): Logs a message to indicate the server is up and running.
  4. http.ListenAndServe(":8080", nil): Starts the HTTP server on port 8080. The first argument (:8080) specifies the address (in this case, port 8080), and the second argument (nil) means it will use the default ServeMux to handle routes.

 

  • Add a Frontend for Testing

For testing the WebSocket functionality, just add an index.html file inside the public/ folder with basic HTML/JS to connect to the WebSocket.

Here’s index.html:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Real-Time Chat</title>
</head>
<body>
    <h2>WebSocket Chat</h2>
    <div id="messages"></div>
    <input id="username" type="text" placeholder="Username" />
    <input id="message" type="text" placeholder="Message" />
    <button onclick="sendMessage()">Send</button>

    <script>
        const socket = new WebSocket('ws://localhost:8080/ws');

        socket.onmessage = function(event) {
            const messages = document.getElementById('messages');
            const message = document.createElement('div');
            message.textContent = event.data;
            messages.appendChild(message);
        };

        function sendMessage() {
            const username = document.getElementById('username').value;
            const message = document.getElementById('message').value;
            socket.send(JSON.stringify({username: username, message: message}));
        }
    </script>
</body>
</html>

Running the Go Backend

To run the Go server:

cd real-time-chat/

go run main.go

The server will now listen for WebSocket connections on ws://localhost:8080/ws.

Then go http://localhost:8080

Image 2

 

 

The React Frontend

Why React?

React is a popular JavaScript library for building user interfaces. Its component-based architecture allows for efficient UI updates, making it perfect for real-time applications where messages need to be displayed instantly upon receipt.

Setting Up the Frontend

Step 1: Create a New React App

Using create-react-app (with TypeScript), we created the frontend. The frontend connects to the WebSocket server and listens for messages to be displayed in real-time.

TypeScript
npx create-react-app chatroom --template typescript

cd chatroom

Step 2: Add Emoji Picker to the Chat

Shell
npm install emoji-mart

Step 3: Create ChatRoom Component

Here’s a breakdown of the React components:

WebSocket Connection: The client connects to the Go WebSocket server.

State Management: The app uses React’s useState and useEffect hooks to manage messages and WebSocket connections.

Real-Time Updates: When the server broadcasts a message, the frontend immediately displays it.

Inside the src/ folder, create a new file called ChatRoom.tsx.

Here's ChatRoom.tsx

TypeScript
import React, { useState, useEffect, useRef, ChangeEvent, KeyboardEvent } from "react";
import Picker from '@emoji-mart/react';
import data from '@emoji-mart/data';

interface Message {
    username: string;
    message: string;
    timestamp: string;
    typing: boolean;
}

const ChatRoom: React.FC = () => {
    const [username, setUsername] = useState<string>("");
    const [message, setMessage] = useState<string>("");
    const [chat, setChat] = useState<Message[]>([]);
    const [typingUser, setTypingUser] = useState<string | null>(null);
    const [ws, setWs] = useState<WebSocket | null>(null);
    const [showPicker, setShowPicker] = useState<boolean>(false);
    const messageRef = useRef<HTMLInputElement>(null);

    // WebSocket connection
    useEffect(() => {
        const socket = new WebSocket("ws://localhost:8080/ws");

        socket.onmessage = (event) => {
            const messageData: Message = JSON.parse(event.data);

            if (messageData.typing && messageData.username !== username) {
                setTypingUser(messageData.username); // Show who is typing
            } else if (!messageData.typing) {
                setChat((prevChat) => [...prevChat, messageData]);
                setTypingUser(null); // Stop showing the typing indicator
            }
        };

        setWs(socket);

        // Cleanup WebSocket connection
        return () => {
            socket.close();
        };
    }, [username]);

    // Handle sending the message
    const sendMessage = () => {
        if (ws && message && username) {
            const timestamp = new Date().toLocaleTimeString();
            const msg: Message = { username, message, timestamp, typing: false };
            ws.send(JSON.stringify(msg));
            setMessage("");
        }
    };

    // Detect when Enter key is pressed
    const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
        if (e.key === "Enter") {
            sendMessage();
        }
    };

    // Handle message input change
    const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
        setMessage(e.target.value);
        if (ws && username) {
            const typingMessage: Message = { username, message: "", timestamp: "", typing: true };
            ws.send(JSON.stringify(typingMessage));
        }
    };

    // Add emoji to the message
    const addEmoji = (emoji: any) => {
        setMessage((prevMessage) => prevMessage + emoji.native);
        setShowPicker(false);
    };

    return (
        <div className="chatroom-container">
            <div className="chatbox">
                <h2>Chat Room</h2>

                <div className="chat-inputs">
                    <input
                        type="text"
                        placeholder="Enter your username"
                        value={username}
                        onChange={(e) => setUsername(e.target.value)}
                    />
                </div>

                <div className="chat-window">
                    {typingUser && <div className="typing-indicator">{typingUser} is typing...</div>}
                    {chat.map((msg, index) => (
                        <div
                            key={index}
                            className={`chat-message ${msg.username === username ? "own-message" : ""}`}
                        >
                            <div className="chat-message-info">
                                <img
                                    src={`https://avatars.dicebear.com/api/initials/${msg.username}.svg`}
                                    alt="avatar"
                                    className="chat-avatar"
                                />
                                <strong className="username">{msg.username}</strong>
                                <span className="timestamp"> at {msg.timestamp}</span>
                            </div>
                            <div>{msg.message}</div>
                        </div>
                    ))}
                </div>

                <div className="chat-inputs">
                    <input
                        ref={messageRef}
                        type="text"
                        placeholder="Enter your message"
                        value={message}
                        onChange={handleChange}
                        onKeyDown={handleKeyDown}
                    />
                    <button onClick={sendMessage}>Send</button>
                    <button onClick={() => setShowPicker(!showPicker)}>😊</button>
                    {showPicker && <Picker data={data} onEmojiSelect={addEmoji} />}
                </div>
            </div>
        </div>
    );
};

export default ChatRoom;
  • WebSocket Connection:

    • When the component mounts (useEffect), a WebSocket connection is established to Go backend at ws://localhost:8080/ws.
    • The WebSocket listens for incoming messages (socket.onmessage) and updates the chat history.
    • The WebSocket connection is closed when the component unmounts to clean up resources.
  • Sending Messages:

    • The sendMessage function sends the user’s message and username as a JSON object through the WebSocket.
    • The Enter key press is captured to send the message when the user presses the Enter key.
  • Displaying Chat:

    • The chat state stores the entire chat history.
    • Each new message is appended to chat and rendered in the chat window.

Step 4: Add ChatRoom Component to App.tsx

TypeScript
import ChatRoom from "./ChatRoom";
import './App.css';

function App() {
  return (
    <div className="App">
      <ChatRoom />
    </div>
  );
}

export default App;

Step 4: Add CSS Styling

Add styling for the chatroom in App.css

CSS
/* Center the chatroom on the page */
.chatroom-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background-color: #f9f9f9;
}

.chatbox {
  width: 500px;
  background-color: #fff;
  padding: 20px;
  border-radius: 10px;
  box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
}

h2 {
  text-align: center;
  font-size: 1.5em;
  margin-bottom: 20px;
}

/* Adjust the input layout */
.chat-inputs {
  margin-bottom: 10px;
  display: flex;
  justify-content: space-between;
}

.chat-inputs input[type="text"] {
  flex-grow: 1;
  padding: 10px;
  border-radius: 5px;
  border: 1px solid #ccc;
  margin-right: 10px;
}

.chat-inputs button {
  padding: 10px 15px;
  border: none;
  background-color: #007bff;
  color: #fff;
  border-radius: 5px;
  cursor: pointer;
}

.chat-inputs button:hover {
  background-color: #0056b3;
}

/* Emoji button */
.chat-inputs button:nth-child(3) {
  background-color: #ffcc00;
}

.chat-inputs button:nth-child(3):hover {
  background-color: #e6b800;
}

/* Chat window styling */
.chat-window {
  height: 300px;
  border: 1px solid #ccc;
  border-radius: 5px;
  padding: 10px;
  overflow-y: auto;
  margin-bottom: 10px;
}

.chat-message {
  display: flex;
  flex-direction: column;
  padding: 10px;
  border-radius: 8px;
  margin-bottom: 10px;
}

.own-message {
  background-color: #d1ffd1;
  align-self: flex-end;
}

.chat-message-info {
  display: flex;
  align-items: center;
}

.username {
  margin-right: 5px; /* Add some space between username and timestamp */
}

.timestamp {
  margin-left: 5px; /* Ensure a small space between 'at' and the timestamp */
}

.chat-avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  margin-right: 10px;
}

.typing-indicator {
  font-style: italic;
  color: gray;
}

/* Adjust button styles */
button {
  cursor: pointer;
}

Running the React Frontend

To start the React frontend:

Shell
cd chatroom/
npm start

The frontend will be available at http://localhost:3000.

Image 3

Real-Time Messaging with WebSocket

When a message is sent from the frontend, it is transmitted via WebSocket to the Go server, which then broadcasts the message to all connected clients. WebSocket enable full-duplex communication, ensuring that messages are received instantly, creating a seamless real-time chat experience.

Example Workflow

  1. User A types a message in the chatbox and presses "Send".
  2. The message is sent to the Go server over a WebSocket connection.
  3. The server broadcasts the message to all connected clients (including User A).
  4. All clients update their chat windows with the new message in real time.

Image 4

Conclusion

In this article, we walked through how to build a simple real-time chat application using Go for the backend and React for the frontend. The application leverages WebSocket to enable real-time communication between clients. Both Go and React are powerful technologies that can be used to build scalable, high-performance applications.

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)