Overview

In this tutorial we will create a multipage web application using django, react and webpack.

This means:

  • Instead of handling routes by react, django will handle all the routes and once a route has been loaded, its upto you whether you want react to create different routes for that particular page or not.
  • Webpack will handle all the staticfiles headache i.e., all the optimizing stuff such as compressing and minifying.
  • Static files bundled by webpack will be served by django.

Let's start

Repository: https://github.com/nitinraturi/django-react-webpack

Dependencies

How to create it from scratch

Install python requirements:

  • Django  (you can install any version, I prefer 2.2 because of its long term support)
  • pip install django==2.2
  • Django Webpack Loader - It consumes the output generated by webpack-bundle-tracker and lets you use the generated bundles in django.
  • pip install django-webpack-loader

Create a django project

django-admin startproject conf .

Create required directories

mkdir {templates,static,frontend}

templates - To serve django templates

static - To serve django static files

frontend - We will install react project inside this directory

Your directory structure will be something like this:

.
├── conf
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── frontend
├── manage.py
├── static
└── templates

Now configure settings.py and urls.py 

# settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, "static_root")
STATICFILES_DIRS = (
    os.path.join(BASE_DIR, "static"),
)
# urls.py

from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
]

if settings.DEBUG:
    urlpatterns += static(settings.STATIC_URL,
                          document_root=settings.STATIC_ROOT)

Create a new app pages

python manage.py startapp pages

Add it in INSTALLED_APPS inside settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'pages'
]

Now inside pages/views.py create two views to serve different urls

# pages/views.py

from django.shortcuts import render

def index1(request):
    return render(request, 'index1.html', {})

def index2(request):
    return render(request, 'index2.html', {})

Create a files urls.py inside pages app and two urls inside pages/urls.py to serve above two views.

touch pages/urls.py
# pages/urls.py

from django.urls import path
from .views import index1, index2

app_name = 'pages'

urlpatterns = [
    path('', index1, name='index1'),
    path('index2', index2, name='index2')
]

Now update conf/urls.py and link pages/urls.py

# conf/urls.py

from django.contrib import admin
from django.urls import path,include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('pages.urls',namespace='pages')),
]

if settings.DEBUG:
    urlpatterns += static(settings.STATIC_URL,
                          document_root=settings.STATIC_ROOT)

Now create the above two html files index1.html and index2.html inside templates directory

<!-- templates/index1.html -->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Django React Webpack - Raturi.in</title>
</head>

<body>
    <ul>
        <li><a href="{% url 'pages:index1' %}">Index1</a></li>
        <li><a href="{% url 'pages:index2' %}">Index2</a></li>
    </ul>

    <center>
        At index1
    </center>
</body>

</html>
<!-- templates/index2.html -->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Django React Webpack - Raturi.in</title>
</head>

<body>
    <ul>
        <li><a href="{% url 'pages:index1' %}">Index1</a></li>
        <li><a href="{% url 'pages:index2' %}">Index2</a></li>
    </ul>

    <center>
        At index2
    </center>
</body>

</html>

Now django's part is done, we have to configure react and webpack. you can verify the changes by running the server

python manage.py runserver 0:8080

Let's setup react part

Change your directory to frontend

cd frontend/

Now create a react app using

npx create-react-app .

Now install required npm packages

npm install --save-dev webpack webpack-cli webpack-bundle-tracker babel-cli babel-core babel-loader babel-preset-env babel-preset-es2015 babel-preset-react babel-register

Now create two files webpack.config.js and .babelrc and a directory for storing your webpack generated bundles inside fronted as assets/dist

touch .babelrc webpack.config.js
mkdir assets assets/dist

Now configure webpack.config.js .babelrc package.json 

webpack.config.json

const path = require("path");
const BundleTracker = require('webpack-bundle-tracker');

var config = {
    context: __dirname,
    entry: {
        'staticfiles': './src/index.js',
    },

    resolve: {
        alias: {
            '@': path.resolve(__dirname, 'src/'),
            '@django': path.resolve(__dirname, '../static/'),
        }
    },
    output: {
        path: path.join(__dirname, './assets/dist'),
        filename: "[name]-[hash].js",
        publicPath: '/static/dist/'
    },


    plugins: [
        new BundleTracker({ filename: './webpack-stats.json' }),
    ],
    devtool: 'cheap-module-eval-source-map',
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: ['babel-loader'],
                include: [
                    path.resolve(__dirname, "src"),
                ],
            },
            {
                test: /\.css$/,
                use: [
                    'style-loader', 'css-loader'
                ]
            },
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    'file-loader',
                ],
            },
        ]
    }
}

module.exports = (env, argv) => {

    if (argv.mode === 'production') {
        config.devtool = 'none';
    }
    return config
};

.babelrc 

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

package.json

{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.5.0",
    "@testing-library/user-event": "^7.2.1",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-scripts": "3.4.1"
  },
  "scripts": {
    "build": "./node_modules/.bin/webpack --mode=production --config webpack.config.js",
    "watch": "npm run start -- --watch",
    "start": "./node_modules/.bin/webpack --mode=development --config webpack.config.js"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-core": "^6.26.3",
    "babel-loader": "^8.1.0",
    "babel-preset-env": "^1.7.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "babel-register": "^6.26.0",
    "webpack": "^4.42.1",
    "webpack-bundle-tracker": "^0.4.3",
    "webpack-cli": "^3.3.11"
  }
}

Now in your conf/settings.py add webpack related settings

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'pages',
    'webpack_loader', # add this
]


FRONTEND_DIR = os.path.join(BASE_DIR,'frontend') # path to frontend directory

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, "static_root")
STATICFILES_DIRS = (
    os.path.join(BASE_DIR, "static"),
    os.path.join(FRONTEND_DIR, "assets"), # path to webpack generated files
)

# Status of files generated by webpack
WEBPACK_LOADER = {
    'DEFAULT': {
        'BUNDLE_DIR_NAME': 'dist/',  # must end with slash
        'STATS_FILE': os.path.join(FRONTEND_DIR, 'webpack-stats.json')
    }
}

Now update your templates index1.html and index2.html to load webpack generated files something like this

{% load render_bundle from webpack_loader %}
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Django React Webpack - Raturi.in</title>
    {% render_bundle 'staticfiles' 'css'%}
</head>

<body>
    <ul>
        <li><a href="{% url 'pages:index1' %}">Index1</a></li>
        <li><a href="{% url 'pages:index2' %}">Index2</a></li>
    </ul>

    <center>
        At index1
    </center>
    {% render_bundle 'staticfiles' 'js' %}
</body>

</html>

In the above code, render_bundle 'staticfiles': staticfiles is the name of the entrypoint and render_bundle will load the files generated by webpack inside dist folder.

How to run the server

  • Run django normally with runserver command

  • To run react open a seperate terminal inside frontend directory  and run

  • npm run watch 
    
    # this command will automatically look for changes for static files and will update the dist folder.
    # You have to just refresh your browser where django is running.
  • In case of production run 
  • npm run build
  • Finally check the source code of html file in browser you can see webpack generated files inside dist folder.

That all.