DEV Community

Cover image for How to Integrate Real-Time Chat in a React and Django Application (WebSockets & Channels)
Shubham Raj
Shubham Raj

Posted on Originally published at urbanshift.vercel.app

How to Integrate Real-Time Chat in a React and Django Application (WebSockets & Channels)

How to Integrate Real-Time Chat in a React and Django Application: A Production Case Study (UrbanShift)

When building an interactive web platform, real-time messaging is no longer a luxuryβ€”it’s an expectation. In this article, I'll walk you through how I architected and implemented a high-performance, real-time chat system for UrbanShift (a relocation and property rental platform) using React on the frontend and Django on the backend.


πŸ“Œ The Problem: Why Real-Time Communication Was Critical

In UrbanShift, tenants looking for rental homes and customers booking packers & movers need immediate answers. Initially, relying on standard HTTP polling or email/SMS alerts caused noticeable lag and poor user experience:

  • High Latency: Buyers and property owners couldn't negotiate in real-time.
  • Server Overload: Continuous HTTP polling repeatedly hit the database with unnecessary queries.
  • Disconnected UX: Users had to manually refresh the page to see if the landlord had responded.

The Feature Goal: Enable instant, bi-directional, peer-to-peer style messaging between two users with persistent chat history, typing feedback, and auto-reconnection.

While developers often search for Socket.io when coming from Node.js, the Django ecosystem provides an idiomatic and scalable counterpart: WebSockets powered by Django Channels & ASGI. Here is how the end-to-end integration works in production.


πŸ’» Clean Code Snippets from UrbanShift

1. Backend: Async WebSocket Consumer (consumers.py)

Django Channels uses an AsyncWebsocketConsumer to handle WebSocket handshakes, room routing, and async broadcast across channel layers without blocking threads.

# backend/chat/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.contrib.auth import get_user_model
from .models import Message, ChatRoom

User = get_user_model()

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        # 1. Extract dynamic room name from URL scope
        self.room_name = self.scope['url_route']['kwargs']['room_name']
        self.room_group_name = f'chat_{self.room_name}'

        # 2. Join the room channel group
        await self.channel_layer.group_add(self.room_group_name, self.channel_name)
        await self.accept()

    async def disconnect(self, close_code):
        # Leave channel group on tab close/disconnect
        await self.channel_layer.group_discard(self.room_group_name, self.channel_name)

    async def receive(self, text_data):
        data = json.loads(text_data)
        message = data['message']
        username = data['username']

        # 3. Save message asynchronously to DB
        await self.save_message(username, self.room_name, message)

        # 4. Broadcast to everyone subscribed to this room group
        await self.channel_layer.group_send(
            self.room_group_name,
            {
                'type': 'chat_message',
                'message': message,
                'username': username
            }
        )

    async def chat_message(self, event):
        # Push message payload to client WebSocket
        await self.send(text_data=json.dumps({
            'message': event['message'],
            'username': event['username']
        }))

    @database_sync_to_async
    def save_message(self, username, room_name, content):
        user = User.objects.get(username=username)
        room, _ = ChatRoom.objects.get_or_create(name=room_name)
        return Message.objects.create(sender=user, room=room, content=content)
Enter fullscreen mode Exit fullscreen mode

2. Frontend: Deterministic Room Generation & WebSocket Hook (Chat.js)

To ensure both User A and User B land in the exact same chat room without hardcoding IDs, we dynamically sort user IDs (e.g., 1_5).

// frontend/src/components/Chat.js
import React, { useState, useEffect, useRef } from 'react';
import useWebSocket from 'react-use-websocket';
import axios from 'axios';

const Chat = ({ currentUserId, targetUserId }) => {
    const [messageHistory, setMessageHistory] = useState([]);
    const [message, setMessage] = useState('');

    // 1. Deterministic 1-on-1 Room Name (e.g., "1_5" regardless of who initiates)
    const roomName = currentUserId < targetUserId 
        ? `${currentUserId}_${targetUserId}` 
        : `${targetUserId}_${currentUserId}`;

    const WS_URL = process.env.NODE_ENV === 'production' 
        ? `wss://urbanshift-project.onrender.com/ws/chat/${roomName}/`
        : `ws://127.0.0.1:8000/ws/chat/${roomName}/`;

    // 2. Persistent, auto-reconnecting WebSocket connection
    const { sendJsonMessage, lastJsonMessage } = useWebSocket(
        roomName ? WS_URL : null,
        { shouldReconnect: () => true }
    );

    // 3. Append incoming socket messages in real-time
    useEffect(() => {
        if (lastJsonMessage !== null) {
            setMessageHistory((prev) => [
                ...prev,
                {
                    sender_name: lastJsonMessage.username,
                    content: lastJsonMessage.message,
                    timestamp: new Date().toISOString()
                }
            ]);
        }
    }, [lastJsonMessage]);

    const handleSend = () => {
        if (!message.trim()) return;
        sendJsonMessage({ message, username: currentUser.username });
        setMessage('');
    };

    return (
        /* Chat UI Rendering */
    );
};
Enter fullscreen mode Exit fullscreen mode

3. Smooth Auto-Scroll & UX Refinements

const messagesEndRef = useRef(null);

// Automatically scroll to the latest message whenever messageHistory changes
useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messageHistory]);
Enter fullscreen mode Exit fullscreen mode

πŸ› οΈ The Solution: Solving Key Production Challenges

Integrating real-time communications in a full-stack production setup always brings architectural hurdles. Here is how I solved them:

1. Database Thread Blocking vs Async ASGI

  • The Problem: Django ORM operations are inherently synchronous. Executing Message.objects.create() directly inside an asynchronous WebSocket consumer (async def receive) raises synchronous-only errors or blocks the event loop.
  • The Fix: Used Channels' @database_sync_to_async decorator to wrap database writes. This cleanly offloads the query to a worker thread while keeping the ASGI event loop ultra-responsive.

2. Dual Architecture (REST API for History + WebSockets for Live Events)

  • Fetching entire message history over WebSockets creates unnecessary parsing overhead on initial render.
  • The Solution: Combined a standard REST endpoint (/api/chat/history/<room>/) via axios on initial mount with instant WSS updates for newly arrived payloads.

3. Cross-Origin (CORS) & WSS Protocol Upgrades in Production

  • In development, ws:// works seamlessly on localhost. However, in production with Vercel (https://) and Render backend (https://), browsers strictly block unencrypted ws:// connections due to mixed-content security policies.
  • The Solution: Implemented environment-aware protocol switching (ws:// vs wss://) and configured CORS/CSRF trusted origins in Django settings.py for both Vercel frontend domains.

πŸ”— Project Links & Live Demo

Feel free to explore the project, inspect the source code, or test out the chat live!


If you found this breakdown helpful, drop a like ❀️ and bookmark πŸ”– it for your next Django + React real-time project!

Top comments (0)