Building your Django and React project using Django Webpack Loader and Webpack Bundle Tracker

Renato Vieira
February 9, 2022
<p>When we start a monorepo project using Django and React, we face a dilemma on how to integrate both parts, specifically on how we allow Django templates to render the JavaScript assets generated by the frontend build. One nice approach to solve this is to use <a href="">Django Webpack Loader</a> with <a href="">Webpack Bundle Tracker</a> to take care of this step in the build pipeline. In this article, we’ll teach you how.</p><h2 id="frontend-setup">Frontend setup</h2><p>We must first deal with setting up the frontend application. For that, we’ll use Webpack Bundle Tracker. It will be added to your project <code>webpack.config.js</code> as a new plugin, and will be responsible to gather all assets that were outputted by Webpack’s pipeline (such as JavaScript and CSS files, images, fonts, etc), as well as store their paths into a new file, which we’ll call “stats file” beyond this point.</p><p>To begin with, we must add Webpack Bundle Tracker to our project’s <code>package.json</code> and install it. You can do both these steps at once by running <code>npm install --save webpack-bundle-tracker</code> on your terminal. Running with <code>--save</code><strong> </strong>instead of <code>--save-dev</code><strong> </strong>will allow us to compile the frontend assets inside a deployment pipeline (more on that later).</p><p>After installing, we must configure the Webpack build. If you’re using <a href="">Create React App</a> to bootstrap the project, you must first run <code>npm run eject</code>. This will allow you to further customize your project build steps, as it will give you access to, among others, the Webpack configuration files. Be aware that ejecting is a one-way operation and can’t be reversed. You can read more about it <a href="">here</a>.</p><p>Below is an example of the <code>webpack.config.js</code> after we add Webpack Bundle Tracker. This file is located at the project root:</p><figure class="kg-card kg-code-card"><pre><code>const path = require('path'); const webpack = require('webpack'); const BundleTracker = require('webpack-bundle-tracker'); module.exports = { context: __dirname, entry: './assets/js/index', output: { path: path.resolve('./dist/'), filename: "[name]-[hash].js" }, plugins: [ new BundleTracker({filename: './webpack-stats.json'}) ], };</code></pre><figcaption>webpack.config.js</figcaption></figure><p>The value stored on <code>output.path</code> represents where the compiled assets are going to be stored. On <code>plugins</code>, we provide an array of instances of the plugins we want to use. In our case, we’ll only use the <code>BundleTracker</code> plugin. On its initialization, it accepts a <code>filename</code> parameter that corresponds to the path of the stats file.</p><p>It’s highly recommended that you don’t keep any generated file (be it the stats file or the compiled assets) in your version control system, so make sure to add <code>webpack-stats.json</code> and the <code>/dist/</code> directory to your <code>.gitignore</code>.</p><h2 id="backend-setup">Backend setup</h2><p>After setting up the frontend part, we must now configure Django to see the compiled assets. You can then install Django Webpack Loader by running <code>pip install django-webpack-loader</code> in your environment. To persist the installed packages so they can be used in other environments, you can run <code>pip freeze &gt; requirements.txt</code> to save them into a file. From this point onwards, you can install the packages listed in the requirements file using <code>pip install -r requirements.txt</code>.</p><p>After the package is installed, we can start configuring the project. As in most Django apps, it all begins within the <code></code> file. We should start by adding the <code>webpack_loader</code> app into the <code>INSTALLED_APPS</code> list.</p><p>You must also add the <code>STATICFILES_DIRS</code><strong> </strong>variable to <code></code>. That should point to the directory where the static files are located and will let Django know where to look.</p><pre><code>STATICFILES_DIRS = ( os.path.join(BASE_DIR, 'dist'), )</code></pre><p>Later in the same file, we must configure the Webpack Loader behavior, by instantiating the <code>WEBPACK_LOADER</code> variable. Below is an example of a basic configuration for it.</p><pre><code>WEBPACK_LOADER = { 'DEFAULT': { 'BUNDLE_DIR_NAME': '/', 'CACHE': not DEBUG, 'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats.json'), 'POLL_INTERVAL': 0.1, 'IGNORE': [r'.+\.hot-update.js', r'.+\.map'], } }</code></pre><p>Let’s do a quick round-up on what each of these properties mean.</p><ul><li><code>DEFAULT</code> is the name that will be used for referencing a configuration across the project. Django Webpack Loader supports multiple setups, so <code>WEBPACK_LOADER</code> is a <code>dict</code> of <code>dict</code>s containing these configurations. The keys of the external dictionary can be named in the way the developers intend, however, the convention is to have at least one configuration named <code>DEFAULT</code>;</li><li><code>BUNDLE_DIR_NAME</code><strong> </strong>is a string that will be prefixed to the assets files paths when loading from the static files storage. Since we have instructed Django to look at <code>dist/</code><strong> </strong>when collecting the static files, while also making Webpack store the files in the root of that directory, we provide the root of the static files directory as the value. If this key is absent from the dictionary, it will default to <code>webpack_bundles/</code>;</li><li><code>CACHE</code> is a property that defines if we should cache the assets files paths (<code>True</code>) or if we should always read the stats file (<code>False</code>). The best setup for this is to define it based on the Django <code>DEBUG</code><strong> </strong>variable, which indicates if we’re in a production environment or not. In production, it’s better to cache the results to improve performance, as the files won’t change after the initial compilation. In development, however, it’s better to never cache the paths, since we want to be constantly re-compiling the frontend assets as we change the code;</li><li>Working in tandem with <code>CACHE</code><strong> </strong>is the <code>POLL_INTERVAL</code>. This tells Django how often in seconds it should fetch the stats file to obtain the newest assets paths. In production environments (<code>DEBUG == False</code>), the value on <code>POLL_INTERVAL</code><strong> </strong>is ignored since it’s assumed that the compiled assets won’t change after the deployment, so there’s no need to fetch the files again;</li><li><code>STATS_FILE</code><strong> </strong>is the path to the stats file generated by Webpack Bundle Tracker. This must match the <code>output.path</code> from frontend’s <code>webpack.config.js</code>;</li><li><code>IGNORE</code><strong> </strong>is a list of regular expressions that will be matched against the files generated during the Webpack compilation. If any file matches these, it’ll be ignored.</li></ul><p>There are also some extra, more advanced properties that can be used for fine-tuning the configurations.</p><ul><li><code>TIMEOUT</code><strong> </strong>is the amount of seconds that Webpack Loader will wait for the compilation to finish before raising an exception. Providing <code>0</code>, <code>None</code><strong> </strong>or leaving this key out of the dictionary will disable timeouts;</li><li><code>LOADER_CLASS</code><strong> </strong>is a string with the name of a Python class that implements a custom Webpack loader. This can be leveraged to implement a new behavior for how the stats file can be loaded, such as from a database or an external url. Below is an example of how to load the stats file from an external source, by extending the <code>webpack_loader.loader.WebpackLoader</code> class.</li></ul><pre><code>import requests from webpack_loader.loader import WebpackLoader class ExternalWebpackLoader(WebpackLoader): def load_assets(self): url = self.config['STATS_URL'] return requests.get(url).json(</code></pre><h2 id="rendering-to-a-template">Rendering to a template</h2><p>After we have the application setup, we can now proceed to leverage Django Webpack Loader utilities to render whatever was developed in the frontend application. What we’ll do is to render the JavaScript files to a Django HTML template using the tools provided by us by the libraries.</p><p>Django Webpack Loader has the built-in template tag <code>render_bundle</code><strong> </strong>which is responsible for including all files from a given entrypoint present on the stats file. It’s used on a Django template as shown below.</p><pre><code>{% load render_bundle from webpack_loader %} {% render_bundle 'main' %}</code></pre><p>It’s recommended to have a single entrypoint per Django template, but you can call <code>render_bundle</code> as many times as you wish in order to render the necessary files since the loader will deduplicate the repeated chunks.</p><pre><code>{% load render_bundle from webpack_loader %} {% render_bundle 'main' %} {% render_bundle 'other_entrypoint' %}</code></pre><p>You are also allowed to include files from a given extension within an entrypoint. For example, you may wish to separately include the CSS and JavaScript files from the <code>main</code> entrypoint, in order to include the former in the template <code>&lt;head&gt;</code> section and the latter before <code>&lt;/body&gt;</code>. This can be achieved by providing a second argument (you can also pass it using the <code>extension</code> keyword) to <code>render_bundle</code> that represents the desired file extension.</p><pre><code>{% load render_bundle from webpack_loader %} &lt;html&gt; &lt;head&gt; {% render_bundle 'main' 'css' %} &lt;/head&gt; &lt;body&gt; {% render_bundle 'main' extension='js' %} &lt;/body&gt; &lt;/head&gt;</code></pre><h2 id="running-in-the-development-environment">Running in the development environment</h2><p>If you’ve started the project using Create React App, you may compile and serve the assets by running <code>npm run start</code>. This will generate the stats file. If a different setup was used, the files can be compiled and served through the <code>npx webpack --config webpack.config.js --watch</code> command.</p><p>For the backend application, it will run as a common Django application, by using the <code>python runserver</code> command.</p><h2 id="usage-in-production">Usage in production</h2><p>Since the frontend generated files (compiled assets, stats file, etc) are left out of the version control system, it’s necessary to set up a production pipeline that will compile and collect the assets for you during the deployment phase.</p><p>The way you set up may vary depending on the platform you’re using (Heroku, AWS, etc), but the configuration should run at least the commands below.</p><pre><code>npm run build python collectstatic --noinput</code></pre><p>The first command will run the frontend build pipeline, which will make Webpack Bundle Tracker create and fill the stats file. The second command will follow-up by manually collecting the static files.</p><p>It’s important to note that in platforms such as Heroku, the <code>collectstatic</code><strong> </strong>step is automatically executed by default during the deployment. We recommend that you disable it by setting the <code>DISABLE_COLLECTSATIC=1</code> environment variable. After that, you must create a <code>post_compile</code> hook to run this step for you. <a href="">Here’s</a> an example on how to implement this flow.</p><h2 id="code-splitting">Code splitting</h2><p>Webpack Bundle Tracker after release 1.0.0 supports code splitting for the frontend assets, in order to generate many bundles that can be separately loaded. You can further read about this topic in the excellent <a href="">Webpack documentation</a>. Below is an example of how to use code splitting in your project.</p><p>Create your entrypoint file to add elements to the DOM. To leverage code splitting, in this example we’ll be using lazy loading inside our component to dynamically import lodash.</p><figure class="kg-card kg-code-card"><pre><code>function getComponent() { return import('lodash') .then(({ default: _ }) =&gt; { const element = document.createElement('div'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); return element; }).catch(error =&gt; 'An error occurred while loading the component'); } getComponent().then((component) =&gt; { document.body.appendChild(component); });</code></pre><figcaption><strong>assets/js/principal.js</strong></figcaption></figure><p>Then, set up your Webpack configuration file like this:</p><figure class="kg-card kg-code-card"><pre><code>module.exports = { context: __dirname, entry: { principal: './assets/js/principal', }, output: { path: path.resolve('./dist/'), // publicPath should match your STATIC_URL config. // This is required otherwise webpack will try to fetch // our chunk generated by the dynamic import from "/" instead of "/dist/". publicPath: '/dist/', chunkFilename: '[name].bundle.js', filename: "[name]-[fullhash].js" }, plugins: [ new BundleTracker({ filename: './webpack-stats.json' }) ] };</code></pre><figcaption><strong>webpack.config.js</strong></figcaption></figure><p>For Webpack 4 users, please change <code>filename: "[name]-[fullhash].js"</code> into<strong> </strong><code>filename: "[name]-[hash].js"</code>, while also changing <code>return import('lodash')</code><strong> </strong>into <code>return import(/* webpackChunkName: "lodash" */ 'lodash')</code>.</p><p>After setting this up, you can render the bundle on your template as usual.</p><figure class="kg-card kg-code-card"><pre><code>{% load render_bundle from webpack_loader %} &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;meta charset="utf-8"&gt; &lt;title&gt;My test page&lt;/title&gt; &lt;/head&gt; &lt;body&gt; &lt;p&gt;This is my page&lt;/p&gt; {% render_bundle 'principal' 'js' %} &lt;/body&gt; &lt;/html&gt;</code></pre><figcaption><strong>index.html</strong></figcaption></figure><h2 id="handling-s3-paths">Handling S3 paths</h2><p>In order to use S3 paths within the project, the values for <code>STATIC_URL</code><strong> </strong>(on Django settings) and <code>publicPath</code><strong> </strong>(on the Webpack production configuration file) must match.</p><p>To keep the values consistent across the project, we’ll use environment variables to store them. In your Django production settings, add the following code.</p><pre><code>from decouple import config AWS_STATIC_BUCKET_NAME = config("AWS_STATIC_BUCKET_NAME") AWS_STATIC_DIRECTORY = config("AWS_STATIC_DIRECTORY") STATIC_URL = f"https://{AWS_STATIC_BUCKET_NAME}{AWS_STATIC_DIRECTORY}/"</code></pre><p>We’re using <a href="">python-decouple</a> to manage the environment variables in the Django side.</p><p>Here, we build the <code>STATIC_URL</code><strong> </strong>value using the values from the environment variables, such as the AWS bucket name and the directory where the files are stored.</p><p>In the Webpack production configuration file, we do the same but using the Javascript notation and syntax.</p><figure class="kg-card kg-code-card"><pre><code>const AWS_STATIC_BUCKET_NAME = process.env.AWS_STATIC_BUCKET_NAME; const AWS_STATIC_DIRECTORY = process.env.AWS_STATIC_DIRECTORY; module.exports = { // ... output: { // ... publicPath: `https://${AWS_STATIC_BUCKET_NAME}${AWS_STATIC_DIRECTORY}/`, // ... }, // ... };</code></pre><figcaption><strong>webpack.config.js</strong></figcaption></figure><h2 id="using-multiple-configurations">Using multiple configurations</h2><p>As previously mentioned in the backend setup section, you can have multiple Webpack configurations in your project. You may add a new entry to the <code>WEBPACK_LOADER</code><strong> </strong>dictionary in the settings file like it’s done below:</p><pre><code>WEBPACK_LOADER = { 'DEFAULT': { 'BUNDLE_DIR_NAME': 'bundles/', 'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats.json'), }, 'OTHER': { 'BUNDLE_DIR_NAME': 'other_bundles/', 'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats-other.json'), } }</code></pre><p>After that, you can reference that configuration when rendering a bundle in the template, by using the <code>config</code> keyword argument.</p><pre><code>{% load render_bundle from webpack_loader %} &lt;html&gt; &lt;body&gt; {% render_bundle 'main' config='DASHBOARD' %} &lt;!-- you can even pair it with other arguments, such as the files’ extension --&gt; {% render_bundle 'main' 'css' 'DASHBOARD' %} {% render_bundle 'main' config='DASHBOARD' extension='css' %} &lt;/body&gt; &lt;/head&gt;</code></pre><h2 id="what-s-next">What’s next?</h2><p>For your next project using Django and React, you may want to try using <a href="">Django React Boilerplate</a>. It’s already loaded with the essential packages and libraries for these kinds of projects and has the initial configuration for using both Django Webpack Loader and Webpack Bundle Tracker along with all their features.</p><p>We’d like to thank Luciano Ratamero for kindly reviewing this blogpost. You can follow Luciano on Twitter <a href="">@lucianoratamero</a>.</p>