Django and React with Live Reload

·

5 min read

In this post I'll show how to integrate Django and React in both your development workflow and production deployment.

What is the main problem we're trying to solve? Ideally, we can work with React as usual. That is, we can make changes and have the React app reload itself. It would also be nice if we don't have to make big changes when going to production.

So how do we accomplish these goals?

We'll serve the React build output from Django using the standard static files system. No extra apps or plugins required. To do that, we'll need to customize our React configuration, so that the build output is placed in our Django static directory instead of the usual location.

Before diving into the React config, it would be helpful to get an overview of the Django project structure.

├── app.env
├── backend
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── db.sqlite3
├── docker-compose.yml
├── Dockerfile
├── frontend
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── models.py
│   ├── static
│   │   └── frontend
│   ├── templates
│   │   └── frontend
│   │       └── index.html
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── manage.py
├── nginx.conf
├── requirements.txt
├── sockets
│   └── gunicorn.sock
└── staticfiles

The React app is up a directory. Updated build outputs are moved into frontend/static/frontend when we do an npm run start in the React app.

Customizing React Build Config

Using create-react-app is probably the most popular way of starting a React project. Part of the appeal is that CRA hides the complexity of Webpack and gives you a good setup that will work in most circumstances. Unfortunately, that paradigm causes a bit of a dilemma. You face a daunting task if you want to customize anything. The standard advice on changing the setup involves npm run eject which will reveal all of the previously hidden Webpack configuration.

The resulting Webpack configuration is over 700 lines long!

Luckily, it doesn't have to be that hard. The CRACO project makes it possible to override specific settings without needing to run the eject command. That means we can have a small override config instead of taking on responsibility for the entire massive Webpack config.

This is the craco.config.js that relocates build outputs to the Django static directory.

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');

module.exports = {
  webpack: {
    configure: (webpackConfig, { env, paths }) => { 
      paths.appBuild = webpackConfig.output.path = path.resolve("../backend/frontend/static/frontend");
      webpackConfig.output.filename = "main.js";
      webpackConfig.output.publicPath = "/static/frontend/"
      webpackConfig.plugins = webpackConfig.plugins.map(plugin => {
        if (plugin.constructor.name === 'MiniCssExtractPlugin') {
          return new MiniCssExtractPlugin({
            filename: "style.css",
          });
        }

        return plugin;
      });

      return webpackConfig;
    }
  },
  devServer: {
    devMiddleware: {
      writeToDisk: true
    }
  },
};

Making React and Django Routing Play Nice

We need to serve index.html from Django and also make sure our frontend routes don't conflict with the backend.

The index.html will load React build outputs using the standard Django static files mechanism.

<!DOCTYPE html>
<html>
  {% load static %}
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" href="{% static 'frontend/style.css' %}" />
    <title>Django and React</title>
  </head>
  <body>
    <div id="root" class="content"></div>
    <script
      type="text/javascript"
      src="{% static 'frontend/main.js' %}"
    ></script>
  </body>
</html>

Now we need to set up a view to serve the index.html file. We also need to make sure that requests to unmatched paths end up serving the same index.html file, so that React routing will work properly.

from django.urls import path, re_path
from .views import index_view

urlpatterns = [path("", index_view), re_path(r"^.*$", index_view)]

The index_view itself is very simple and just returns the rendered index.html template.

from django.shortcuts import render

def index_view(request):
    return render(request, "frontend/index.html", context=None)

Importantly, when we include the frontend app URLs in the main urls.py file, we include it last so that any Django routes take precedence.

from django.contrib import admin
from django.urls import path, include

urlpatterns = [path("admin/", admin.site.urls), path("", include("frontend.urls"))]

Going to Production

I've set up a docker-compose.yml to mirror a production setup to prove that this also works in that context.

I'm using Nginx and Gunicorn for a mock production setup. The compose file defines Django and Nginx as the services.

version: "3.7"

services:
  nginx:
    image: nginx:1.14.2
    restart: unless-stopped
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./staticfiles:/usr/src/app/staticfiles
      - ./sockets:/usr/src/app/sockets
    ports:
      - "80:80"

  app:
    build:
      context: .
    restart: unless-stopped
    volumes:
      - $PWD:/usr/src/app
      - ./sockets:/usr/src/app/sockets
    env_file:
      - app.env

The Django app and Nginx are connected with a Unix domain socket. The Nginx config is very simple, and serves static assets out of the staticfiles directory for any URL that matches the /static/ pattern. All other requests go to the app container.

events {}

http {
    sendfile on;

    server {
        listen 80;

        location / {
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_pass http://unix:/usr/src/app/sockets/gunicorn.sock;
        }

        location /static/ {
            include /etc/nginx/mime.types;
            alias /usr/src/app/staticfiles/;
            expires 30d;
            access_log off;
        }
    }
}

This setup is fairly simple and convenient. The only downside is that Django is forced to serve index.html any time the browser reloads. Alternately, you could have Nginx serve the index.html file for unknown routes, but then routing to Django would have to happen only at a specific prefix, such as URLs beginning with /api/.

I don't think Django having to route for React paths is a big issue. Maybe it is if you're serving a huge amount of traffic? The other potential issue is that if your Django app is broken, so is your React routing (on page reloads).