Django Channels and React: a match made in heaven


   


Django Channels and React are some of the most powerful tools out there when it comes to building real-time messaging applications. However, there are not many resources out there on how to use them together.

I will show you how to consume a websockets backend powered by Django Channels with a React and Zurb Foundation frontend. You will learn how Babel and Webpack make all of this possible, and how to work with dates nicely with Moment.js.

Setting up the project


$ mkdir chat_project && cd chat_project
$ virtualenv -p /usr/bin/python3.8 venv
$ source venv/bin/activate
$ django-admin startproject project
$ cd project
$ ./manage.py startapp backend
$ ./manage.py startapp frontend
$ sudo apt install redis-server


The backend app will contain our Python code.  This app will take care of doing some business logic and serving Websockets with Django Channels.  It will also enable Channel Layers (a channels feature I will explain later) with Redis, and serve the bundle produced by Webpack.

The frontend app will contain our React app, all the node modules and some CSS. We will configure Babel and Webpack here.

Your initial structure should look like this:

├── backend
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── frontend
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── manage.py
└── project
├── asgi.py
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py


project/settings.py

INSTALLED_APPS = [
# own
'backend',
'frontend',

# Django
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]


Building the backend


The backend of this app is a modified version of the chat you build on the Django Channels tutorial. I have added small improvements here and there, and I have changed the structure in a way that makes more sense to me.

I have deliberately chosen not to use async Python for this tutorial. I think async Channels deserves its own post, and by doing this I can focus on the basics of integrating React with Django instead.


Views and URLs: look mum, no logic!


project/backend/views.py

from django.shortcuts import render

def index(request):
return render(request, 'frontend/index.html')

def room(request, room_name):
return render(request, 'frontend/room.html')


project/backend/urls.py

from django.urls import path

from . import views


urlpatterns = [
    path('', views.index, name='index'),
    path('<str:room_name>/', views.room, name='room'),
]


That's right, all our views do is render a couple of templates grabbed from the frontend app. You will understand this soon, for now let's add the base and index templates to the frontend app.


frontend/templates/frontend/base.html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">

<!-- FOUNDATION -->
<script
src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
integrity="sha256-4+XzXVhsDmqanXGHaHvgh1gMQKX40OUvDEBTu8JcmNs="
crossorigin="anonymous"></script>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/foundat
ion.min.css" integrity="sha256-ogmFxjqiTMnZhxCqVmcqTvjfe1Y/ec4WaRj/aQPvn+I=" crossorigin="anonymous">

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/foundation.min.js" integ
rity="sha256-pRF3zifJRA9jXGv++b06qwtSqX1byFQOLjqa2PTEb2o=" crossorigin="anonymous"></script>
<!-- END FOUNDATION -->

<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.26.0/moment.min.js" integrity="sh
a256-5oApc/wMda1ntIEK4qoWJ4YItnV4fBHMwywunj8gPqc=" crossorigin="anonymous"></script>

{% load static %}
<link rel="stylesheet" type="text/css" href={% static "frontend/textarea.css" %}>
<link rel="stylesheet" type="text/css" href={% static "frontend/base.css" %}>

<title>Channels Chat</title>
</head>

<body>
{% block body %}
{% endblock %}
</body>

</html>


By using a base template we can avoid repeating a lot of HTML. From now on every template will import Foundation, Moment.js and some CSS by extending this template.

Whatever a template renders needs to go inside a {% block body %}.


frontend/templates/frontend/index.html

{% extends 'frontend/base.html' %}
{% block body %}
<div class="grid-x cell align-center large-8">
<main>
<input id="room-name-input" type="text" size="100" placeholder="What chat room would you like to enter?"/><br/>
<input id="room-name-submit" type="button" class="button" value="Enter" />
</main>
</div>
<script>
document.querySelector('#room-name-input').focus();
document.querySelector('#room-name-input').onkeyup = function(e) {
if (e.keyCode === 13) { // enter, return
document.querySelector('#room-name-submit').click();
}
};

document.querySelector('#room-name-submit').onclick = function(e) {
var roomName = document.querySelector('#room-name-input').value;
roomName = roomName.replace(/ /g, '_');
window.location.pathname = roomName + '/';
};
</script>

{% endblock %}


Foundation provides uses a 12 column system just like Bootstrap, and it provides large, medium and small classes. Unlike flexbox based grids, Foundation's XY Grid can control elements both vertically and horizontally, though that's beyond the scope of this post!

Other tags such as align-center are very intuitive and should be familiar to any Bootstrap user.


frontend/templates/frontend/room.html

{% extends 'frontend/base.html' %}

{% block body %}

<body>
    <div id="app" class="grid-x cell large-8 align-center">
    <!-- React will load here -->
    </div>

{% load static %}
    <script src="{% static "frontend/main.js" %}"></script>
    <script>
     $(document).foundation();
    </script>
</body>

{% endblock %}


frontend/static/frontend/base.css

body {
padding: 5px;
}


frontend/static/frontend/textarea.css

#chat-message-input {
    resize: none;
    height: 80px;
}


At this point you should see something like this:




What is a websocket?


Websocket is another communications protocol, just like HTTP and UDP.

Unlike HTTP, websocket is stateful, meaning information persists between requests. Websockets allow for real-time communication between clients and servers, also known as bidirectional communication.

You can read about how websocket URLs are formed here.

Websockets are in the backend of many modern web projects such as Slither.




Working with routers and consumers


When you connect to a Channels app, your connection is split in two parts: a scope and the events that are generated.

In our app, events are chat messages with some text and time data.

A scope contains information about the user, their machine, etc. One of the limitations of HTTP is that this information is destroyed after each request, while one of the benefits of Websockets is that they allow this information to be shared for a while.


Classic Django
Django Channels
ViewsConsumers
URLsRouter
This table shows a rough equivalence between these concepts.

Consumers have that name because they consume the events I introduced above. When you write a Django Channels app your logic will typically be done by them (that's why our views only render a couple of templates).


project/backend/consumers.py

import datetime
import json

from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer


class ChatConsumer(WebsocketConsumer):
def connect(self):
"""
Connect to a chat room
Spaces are replaced like this: 'My new room' -> 'My_new_room'
"""

self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_name = self.room_name.replace(' ', '_')
self.room_group_name = 'chat_%s' % self.room_name

# Join room group
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)

self.accept()

def disconnect(self, close_code):
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name,
self.channel_name
)


WebsocketConsumer provides a connect() method to accept an incoming websocket connection. The idea here is to overwrite that method to add the user's channel to his room group before the connection is accepted. The user can then start sending messages to other users in the room.

By default, the disconnect method does nothing. Here we use it to remove the user's channel from the room group.


class ChatConsumer(WebsocketConsumer):    

        
def connect(self):
     ...

    def disconnect(self):
...

def receive(self, text_data):

"""
Receive a message and broadcast it to a room group
UTC time is included so the client can display it in each user's local time
"""

text_data_json = json.loads(text_data)
message = text_data_json['message']
utc_time = datetime.datetime.now(datetime.timezone.utc)
utc_time = utc_time.isoformat()

async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
'utc_time': utc_time,
}
)

def chat_message(self, event):
"""
Receive a broadcast message and send it over a websocket
"""

message = event['message']
utc_time = event['utc_time']

# Send message to WebSocket
self.send(text_data=json.dumps({
'message': message,
'utc_time': utc_time,
}))


We could simply use the server's time for the messages, but then the time would only be correct for users in the same timezone. By returning the UTC time, the client can do the maths and display the correct time.

Here we tell our app to look at ws/python_fans when we enter the Python Fans room, and to process any events happening there with a ChatConsumer instance:
project/backend/routing.py

from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
re_path(r'ws/(?P<room_name>\w+)/$', consumers.ChatConsumer),
]


Enough chit-chat, time to install the Channels app.

project/settings.py

INSTALLED_APPS = [
# own
'backend',
'frontend',

#3rd party
'channels',

# Django
...
]


If you start the development server with ./manage.py runserver now, Django will throw an error.

CommandError: You have not set ASGI_APPLICATION, which is needed to run the server.

Unlike a typical Django app, Channels requires us to define an ASGI_APPLICATION.

ASGI_APPLICATION = 'project.routing.application'


ASGI apps can handle HTTP and websockets.

One of the things the scope of a connection does is informing the app of what type of connection it is. By adding the ProtocolTypeRouter below, the app will look at the URLs defined at backend/routing.py when it's dealing with a websocket connection.

project/routing.py

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing


application = ProtocolTypeRouter({
'websocket': AuthMiddlewareStack(
URLRouter(
backend.routing.websocket_urlpatterns
)
),
})


Working with channel layers


If we were using an old-school approach, messages would be sent by doing a POST request, and the app would save them to the database. The client would then retrieve them with a GET request, likely with Ajax long polling. Not terrible, but websockets make our life much easier.

Channel layers are one of the key features of Django Channels. When channels share the same group they can easily broadcast messages to each other by using websockets (instead of HTTP and SQL).

The official implementation uses Redis as a backing store.


project/settings.py

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}



Building the frontend


This blog post has been a huge source of inspiration for me, and it contains my favourite React + Django setup so far. You might as well use that post to set it up, but I will provide some additional explanations to make it less overwhelming.

First of all, I strongly advise against installing Node with apt if you are using Ubuntu or a Debian based OS, as you will run into all sorts of problems. Node moves fast and their repositories don't keep up with it (unlike rolling release distros such as Arch).

$ sudo apt install curl
$ curl -sL https://deb.nodesource.com/setup_12.x |
bash -
$ sudo apt install -y nodejs

If you run npm --version it should output at least 6.x.x.


Let's generate a package.json. This file will contain various things related to our app such as build commands and package dependencies.

$ cd frontend
$ npm init -y


Whenever we install a package with npm, we can choose to add it to the dependencies at package.json with --save-dev. This is similar to what requirements.txt does in a Python environment.

$  npm i webpack webpack-cli --save-dev
$ npm i @babel/core babel-loader @babel/preset-env @babel/preset-react --save-dev
$ npm i react react-dom --save-dev

You have probably noticed that npm produces some verbose output. Most of the times you can safely ignore it. If you check package.json, there should be something like this:

  "devDependencies": {
"@babel/core": "^7.10.1",
"@babel/preset-env": "^7.10.1",
"@babel/preset-react": "^7.10.1",
"babel-loader": "^8.1.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11"
},


Webpack is a module bundler, which is a cool way of saying it produces a Javascript file that can be used to serve the whole HTML5 application (including React, which is just a JS library anyway!).

The Babel transcompiler can make bleeding-edge Javascript run in most browsers with techniques such as polyfills. As for React DOM, it's the part of React that takes care of rendering components in the browser.


Configuring Babel and Webpack


frontend/.babelrc

{
    "presets": [
        "@babel/preset-env", "@babel/preset-react"
    ]
}

frontend/webpack.config.js

module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
]
}
};


Adding a React component


src/index.js

import App from "./components/App";


src/components/App.js

import React, { Component } from "react";
import { render } from "react-dom";


class App extends Component {
constructor(props) {
super(props);
this.state = {
messages: [],
};
}


componentDidMount(){
const roomName = location.pathname.substr(1);

var socketPath = 'ws://'
+ window.location.host
+ '/ws/'
+ roomName;

const chatSocket = new WebSocket(
socketPath
);

chatSocket.onmessage = (e) => {
var data = JSON.parse(e.data);
var message = {text: data.message, date: data.utc_time};
message.date = moment(message.date).local().format('YYYY-MM-DD HH:mm:ss');

let updated_messages = [...this.state.messages];
updated_messages.push(message);
this.setState({messages: updated_messages});
};

chatSocket.onclose = (e) => {
console.error('Chat socket closed unexpectedly');
};

document.querySelector('#chat-message-input').focus();
document.querySelector('#chat-message-input').onkeyup = (e) => {
this.clickSubmitMessage
};

document.querySelector('#chat-message-submit').onclick = (e) => {
var messageInputDom = document.querySelector('#chat-message-input');
var message = messageInputDom.value;

chatSocket.send(JSON.stringify({
'message': message
}));
messageInputDom.value = '';
};
}

render() {
return (
<div>

{this.state.messages.map(function(item, i){
return <div key={i} id="message" className="card">

<div className="cell large-4">{item.text}</div>
<div className="cell large-2 text-right"><small>{item.date}</small></div>
</div>
;}
)}


<textarea id="chat-message-input" type="text" cols="100" /><br />
<input id="chat-message-submit" type="button" className="button" value="Send" />

</div>
);
}
}


export default App;

const container = document.getElementById("app");
render(<App />, container);


Introducing React and breaking down the app


There are many excellent React tutorials out there, but I will do my best at explaining the basics using the chat app above.


class App extends Component {
constructor(props) {
super(props);
this.state = {
messages: [],
loaded: false,
placeholder: "Loading"
};


React components are Javascript classes (and more recently functions as well) that return React elements. React elements are very similar to their HTML counterparts, with some small differences such as using camelcase for attributes and writing className instead of class.

Javascript forces you to call super before you use the this keyword to prevent horrific inheritance bugs.
React components store data in a special Javascript object called state. Instead of modifying it directly, you use the setState method whenever you want to update it, with a syntax like setState({myVariable: 'I love Django'})

You can then use a syntax like { this.state.myVariable } inside React's render loop to render 'I love Django'.


    componentDidMount(){
const roomName = location.pathname.substr(1);

var socketPath = 'ws://'
+ window.location.host
+ '/ws/'
+ roomName;

const chatSocket = new WebSocket(
socketPath
);


React components call several methods in a certain order when they are rendered, updated and removed. These methods are known as lifecycle methods, since they allow you to track the lifecycle of a component.

There are many of these methods, but this app is only using three: constructor, render and ComponentDidMount. They all belong to the mounting or rendering stage.Modern browsers support websockets natively. When the app component has been rendered, we create a websocket connection using the chat room's name.


 chatSocket.onmessage = (e) => {
var data = JSON.parse(e.data);
var message = {text: data.message, date: data.utc_time};
message.date = moment(message.date).local().format('YYYY-MM-DD HH:mm:ss');

let updated_messages = [...this.state.messages];
updated_messages.push(message);
this.setState({messages: updated_messages});
};

chatSocket.onclose = (e) => {
console.error('Chat socket closed unexpectedly');
};


Quoting the MDN docs, The onmessage property is an EventHandler that is called when a message is received from the server. The arrow syntax is the equivalent of using a lambda in Python.

Moment.js provides a moment object with a local() method. By using this method we obtain the date in the user's local timezone, deriving it from the UTC time returned by the server.

Finally, we update the component's state with the new message.


	document.querySelector('#chat-message-submit').onclick = function(e) {
var messageInputDom = document.querySelector('#chat-message-input');
var message = messageInputDom.value;

chatSocket.send(JSON.stringify({
'message': message
}));
messageInputDom.value = '';
};

When the button is clicked, we use Websocket's send method to submit the message.


    render() {
return (
<div>

{this.state.messages.map(function(item, i){
return <div key={i} id="message" className="card">

<div className="cell large-4">{item.text}</div>
<div className="cell large-2 text-right"><small>{item.date}</small></div>
</div>
;}
)}


<textarea id="chat-message-input" type="text" cols="100" /><br />
<input id="chat-message-submit" type="button" className="button" value="Send" />

</div>
);
}
}


render() is where all the magic happens.

Javascript's map function is just like the Python function with the same name. However, it can also be used like Python's enumerate, by obtaining each item of a list with an index.

There are two React requirements that must be met in the render method. One is that these elements need to be wrapped with div tags, and the other one is that list items need a key.

We render the list of messages by wrapping it with the { } syntax.

Bundling modules


$ cd frontend

Let's write a webpack command.

package.json

"scripts": {
  "dev": "webpack --mode development ./src/index.js --output ./static/frontend/main.js"
}

You can now go to frontend and do

$ npm run dev

to create main.js, a bundle that Django will render at the room template. You will need to run this command every time you make a change to the React component.


That's all for now! Test the app at http://127.0.0.1:8000, it should behave like the first GIF of this post.

With websockets the possibilities are endless. Some project ideas are a tic-tac-toe game and a simple Whatsapp clone.


Refactoring the React code


Even though the frontend app doesn't have major bugs, the code is far from being great. You can learn how to refactor it using ESLint and Airbnb's style guide on the next post.