WittCode💻

Create a Group Chat with React Flask Socket.io

By

Learn how to create a group chat with React, Flask, and Socket.io.

Table of Contents 📖

Project Demo

Below is what we are going to build. When a user visits the provided URL, they will join a group chat. The style of the chat mimics that of iMessage. Sent messages are in blue on the right while received are in gray on the left.

Image

[object Object],[object Object],[object Object]

Project Architecture

Below is the architecture of the project. Webpack will build the React application and serve it up to the client. The React app then forms a connection with the Flask server. As they are running on different origins, we will need to handle CORS as well.

Image

Environment Variable Setup

ENV=development

WEBPACK_DEV_SERVER_PORT=1234
WEBPACK_DEV_SERVER_HOST=localhost
WEBPACK_DEV_SERVER_URL=http://localhost:1234

FLASK_PORT=1235
FLASK_HOST=localhost
FLASK_URL=http://localhost:1235

Python Project

python3 -m venv .venv
source .venv/bin/activate
pip install Flask flask-socketio py-mon python-dotenv
from flask import Flask, request
from flask_socketio import SocketIO, emit
from dotenv import load_dotenv
import os

# Take the environment variables from .env file
load_dotenv()
FLASK_HOST = os.environ.get("FLASK_HOST")
FLASK_PORT = os.environ.get("FLASK_PORT")
WEBPACK_DEV_SERVER_URL = os.environ.get("WEBPACK_DEV_SERVER_URL")

# Create Flask app
app = Flask(__name__)
# Wrap Flask app with SocketIO, creates a Flask-SocketIO server
# Specify webpack origin is allowed to connect to this server.
socket_io = SocketIO(app, cors_allowed_origins=WEBPACK_DEV_SERVER_URL)

# Handle connections
@socket_io.on("connect")
def connect():
  # The request.sid is a unique ID for the client connection.
  # It is added by SocketIO
  print(f'Client connected: {request.sid}')

# Handle the data event. This is a user defined event. In other words,
# it is not reserved like connect and disconnect.
@socket_io.on("data")
def handle_data(data):
  # Emits a SocketIO event to one or more connected clients.
  # Broadcast True sends the message to all clients.
  emit("data", {'socketId': request.sid, 'data': data}, broadcast=True)

# Handle disconnections
@socket_io.on("disconnect")
def disconnect():
  print(f'Client disconnected: {request.sid}')

# Run the application at the provided host and port
if __name__ == "__main__":
  socket_io.run(app, host=FLASK_HOST, port=FLASK_PORT)

React Project Setup

npm i socket.io-client

We install the client socket.io library on the client to connect to the Flask socket.io server.

Gluing the DOM to React and Global Styles

import {createRoot} from 'react-dom/client';
import App from './components/App';
import './styles/globals.css';

const root = createRoot(document.getElementById('root'));
root.render(<App />);
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>
* {
  margin: 0;
  padding: 0;
  font-family: 'Roboto', sans-serif;
  box-sizing: border-box;
}

:root {
  --own-message: #218aff;
  --message: #d8d8d8;
}

body {
  height: 100vh;
  width: 100vw;
}

useWebSocket Hook

import {useState, useEffect} from 'react';
import {io} from 'socket.io-client';

/**
 * On mount, connect to the socket.io server
 * @param {*} url 
 * @param {*} handleData 
 * @returns 
 */
export default function useWebSocket(url, handleData) {
  const [socket, setSocket] = useState(null);

  useEffect(() => {

    // Connect to the flask server
    const socketClient = io(url);

    socketClient.on('connect', () => {
      console.log('Connected to server');
    });

    socketClient.on('disconnect', () => {
      console.log('Disconnected from server');
    });

    // To make the hook more generic, accept a handleData
    // function that is passed to the socket.
    socketClient.on('data', (data) => {
      handleData(data);
    });

    setSocket(socketClient);

    // On unmount, close the socket
    return () => {
      socketClient.close();
    }
  }, []);

  return socket
}

Chat Bubble

import * as styles from '../styles/ChatBubble.module.css'

/**
 * The bubble that houses the message. The styles
 * applied are different depending on who sent the message.
 * @param {*} props 
 * @returns 
 */
export default function ChatBubble(props) {
  const {message, socketId} = props
  const ownMessage = message.socketId === socketId


  return (
    <div className={styles.messageContainer}>
      <p className={ownMessage ? styles.messageOwnUsername : styles.messageGuestUsername}>
        {ownMessage ? 'You' : message.socketId}
      </p>
      <div 
        className={`
          ${ownMessage ? styles.ownMessage : styles.guestMessage}
          ${styles.message}`}
      >
        {props.message.data}
      </div>
    </div>
  )
}
p {
  color: var(--message);
  font-size: x-small;
  margin-bottom: 4px;
}

.messageContainer {
  display: flex;
  flex-direction: column;
}

.messageOwnUsername {
  align-self: flex-end;
}

.messageGuestUsername {
  align-self: flex-start;
}

.message {
  padding: 10px;
  border-radius: 5px;
  margin-bottom: 10px;
  width: fit-content;
}

.ownMessage {
  display: inline-block;
  align-self: flex-end;
  background-color: var(--own-message);
  color: white;
  text-align: right;
}

.guestMessage {
  align-self: flex-start;
  background-color: var(--message);
}

Chat Input

import {useState} from 'react';
import * as styles from '../styles/ChatInput.module.css'

/**
 * Accepts user input from the text input and then uses
 * the provided socket to send the message to the server.
 * The server then broadcasts it to all connected clients.
 * @param {*} props 
 * @returns 
 */
export default function ChatInput(props) {
  const [message, setMessage] = useState('');

  const sendMessage = () => {
    props.socket.emit('data', message);
    setMessage('');
  }

  return (
    <div className={styles.chatInput}>
      <input 
        value={message} 
        onKeyDown={(e) => e.key === 'Enter' && sendMessage()} 
        onChange={(e) => setMessage(e.target.value)} 
        type='text'
      >
      </input>
      <button onClick={sendMessage}>Send</button>
    </div>
  )
}
input {
  flex-grow: 1;
  resize: none;
  padding: 10px;
  font-size: 16px;
  border: 1px solid var(--message)
}

input:focus {
  outline: none;
}

button {
  padding: 10px;
  font-size: 16px;
  border: none;
  cursor: pointer;
}

.chatInput {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 8px;
}

Chat Box

import ChatBubble from "./ChatBubble"
import * as styles from '../styles/ChatBox.module.css'

/**
 * Houses the chat bubbles that display the messages
 * @param {*} props 
 * @returns 
 */
export default function ChatBox(props) {
  
  return (
    <div className={styles.chatBox}>
      {props.messages.map((message, i) => (
        <ChatBubble key={i} socketId={props.socket.id} message={message} />
      ))}
    </div>
  )
}
.chatBox {
  width: 100%;
  height: 100%;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  padding: 8px;
}

App Component

import {useState} from 'react';
import useWebSocket from '../hooks/useWebSocket';
import * as styles from '../styles/App.module.css'
import ChatInput from './ChatInput';
import ChatBox from './ChatBox';

/**
 * The main container for the application. Displays
 * a title, the box housing the messages, and the input
 * element for sending messages.
 * @returns 
 */
export default function App() {
  const [messages, setMessages] = useState([]);
  const socket = useWebSocket(
    process.env.FLASK_URL, (data) => {setMessages(prevMessages => [...prevMessages, data])});

  // Loop through the users and display them
  return (
    <div className={styles.app}>
      <h1>WittCepter Chat</h1>
      <ChatBox socket={socket} messages={messages} />
      <ChatInput socket={socket} />
    </div>
  );
}
.app {
  height: 100vh;
  padding: 8px;
  width: 100vw;
  display: flex;
  flex-direction: column;
  align-items: center;
}

Running the Application

ERROR: Make sure to run the Flask server first so that our React app has a server to connect to!

pymon ./server.py
* Running on http://localhost:1235
npm start
> start
> webpack-dev-server --open --config webpack.config.cjs

<i> [webpack-dev-server] [HPM] Proxy created: /api  -> http://localhost:1235
<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:1234/, http://[::1]:1234/