Using React to Progressively Enhance a Django App
Tags: python, django, react
Published: 2023-08-17 10:42:55

### Introduction

In this tutorial, you will create a Hero Builder application using Django and React.  Hero Builder will allow users to build a character and distribute points across attributes such as strength, dexterity, and intelligence.

Django is a high-level, open-source web framework written in Python that encourages rapid development and clean, pragmatic design. Released in 2005, it follows the "batteries-included" philosophy, providing developers with a rich set of tools and utilities out of the box, including an ORM (Object-Relational Mapping), an admin interface, and form handling, to name a few. Django emphasizes the DRY (Don't Repeat Yourself) principle and reusability of components, allowing developers to build robust, scalable, and maintainable web applications with less code.

React, often referred to as React.js, is an open-source JavaScript library developed by Facebook for building user interfaces or UI components. Introduced in 2013, it allows developers to construct complex, interactive UIs using a component-based architecture. Each component in React manages its own state and renders it to the DOM, making it easier to develop and reason about modular pieces of an application.  Over the years, React has become a foundational tool in modern web development, powering many of today’s most popular websites and applications.

In this application, the Django framework will serve the frontend, while React will take over specific parts of the page that require dynamic interactions.

### Prerequisites

To build this application, you will need to complete the following:

- Install Node.js and NPM
- Install Python3

### Setting up the Django Project

In this section you will set up the Django project and verify that all prerequisites are in place.

Begin by using the **django-admin** tool to create a new Django project.

```
django-admin startproject hero_builder
```

Now create a new application within the **hero_builder** directory.

```
cd hero_builder
python3 manage.py startapp hero
```

Run the initial migrations for the SQLite development database.

```
python3 manage.py migrate
```

Start the development server in order to verify that everything is working so far.

```
python3 manage.py runserver
```

You should be able to visit **http://localhost:8000** now.  Django will serve a placeholder page.

<img src="https://circumeo.io/static/images/djangostart.png" style="width: 100%"/>

### Registering the Hero Application

You should now register the **hero** app that was created in earlier steps.  Open the **hero_builder/settings.py** file in your editor and add the **hero** app to the **INSTALLED_APPS** list.

```python
INSTALLED_APPS = [ 
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "hero",
]
```

Save your changes to the **settings.py** file.

### Defining the Character Model

The **hero** app will have a Character model to store the distribution of points across attributes.  Open **hero/models.py** and add the following.

```python
from django.db import models

class Character(models.Model):
    strength = models.PositiveIntegerField(default=20)
    dexterity = models.PositiveIntegerField(default=20)
    health = models.PositiveIntegerField(default=20)
    intelligence = models.PositiveIntegerField(default=20)
    charisma = models.PositiveIntegerField(default=20)

    name = models.CharField(max_length=200)

    def __str__(self):
        return self.name
```

Now you will add and run the migration necessary to create the Character model.

```
python manage.py makemigrations hero
```

To run the migration:

```
python manage.py migrate hero
```

### Setting up the Django Forms and Views

The Hero app views will use a simple Django form class to validate character data.  Open **hero/forms.py** and add the following.

```python
from django import forms

class CharacterForm(forms.Form):
    strength = forms.IntegerField(min_value=0, initial=20)
    dexterity = forms.IntegerField(min_value=0, initial=20)
    health = forms.IntegerField(min_value=0, initial=20)
    intelligence = forms.IntegerField(min_value=0, initial=20)
    charisma = forms.IntegerField(min_value=0, initial=20)

    def clean(self):
        cleaned_data = super().clean()
        total = (
            cleaned_data.get("strength", 0)
            + cleaned_data.get("dexterity", 0)
            + cleaned_data.get("health", 0)
            + cleaned_data.get("intelligence", 0)
            + cleaned_data.get("charisma", 0)
        )

        if total > 100:
            raise forms.ValidationError("The total attributes must not exceed 100.")
        return cleaned_data
```

The form class will validate that each attribute is a positive number and that the total does not exceed 100 points.

Next you'll use this form class as part of the character view, where the browser will make a form POST request to create characters.  Open the **hero/views.py** file and add the following.

```python
from django.shortcuts import render, redirect, get_object_or_404
from .models import Character
from .forms import CharacterForm

def home(request):
    form = CharacterForm()
    return render(request, "hero/home.html", {"form": form})

def character(request, character_id=None):
    if request.method == "POST":
        form = CharacterForm(request.POST)
        if form.is_valid():
            character = Character(
                strength=form.cleaned_data["strength"],
                dexterity=form.cleaned_data["dexterity"],
                health=form.cleaned_data["health"],
                intelligence=form.cleaned_data["intelligence"],
                charisma=form.cleaned_data["charisma"],
            )
            character.save()
            return redirect("character_with_id", character_id=character.pk)
    else:
        character = get_object_or_404(Character, id=character_id)

    return render(request, "hero/character.html", {"character": character})
```

The home view renders the character creation page.  The character page will render the attributes of the created character in a read-only table during a GET request, and will create a new character during a POST request.

### Defining the Django Templates

Now you will define the Django templates that are used in the Hero app.  

The **base.html** template will act as a foundation, so that all the common script and style imports happen in one place.

Open **hero/templates/hero/base.html** and add the following.

```
<!DOCTYPE html>
<html lang="en">
{% load static %}
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Hero Builder</title>
    
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>

    <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>

    {% block extra_js %}{% endblock extra_js %}
    {% block extra_css %}{% endblock extra_css %}
</head>
<body>
    {% if messages %}
        {% for message in messages %}
            <div class="alert alert-danger alert-bottom alert-dismissible fade show" role="alert">
                {{ message }}
                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
            </div>
        {% endfor %}
    {% endif %}
    <nav class="navbar navbar-expand-lg">
        <div class="container-fluid">
            Hero Builder
        </div>
    </nav>

    <div>
        {% block content %}{% endblock content %}
    </div>
</body>
</html>
```

This template loads the minified production source code for the React and ReactDOM libraries.  This will be important later when you include the React web component that you are going to build in the next few steps.

Now you will write the **home.html** template where the React component representing the hero editor is contained.

```
{% extends "hero/base.html" %}
{% load static %}

{% block extra_js %}
<script src="{% static 'hero/js/main.js' %}"></script>
{% endblock %}

{% block extra_css %}
<link href="{% static 'hero/css/main.css' %}" rel="stylesheet">
{% endblock %}

{% block content %}
<div class="d-flex justify-content-center align-items-center vh-100">
    <div class="w-75 p-4 mb-4 border">
        <hero-attribute-editor></hero-attribute-editor>
        <form method="POST" action="{% url 'character' %}">
            {% csrf_token %}
    
            {{ form.strength.as_hidden }}
            {{ form.dexterity.as_hidden }}
            {{ form.health.as_hidden }}
            {{ form.intelligence.as_hidden }}
            {{ form.charisma.as_hidden }}

            <button type="submit" class="btn btn-primary">Submit</button>
        </form>
    </div>
</div>
{% endblock %}
```

If you were to view this page rendered in the browser right now, the **hero-attribute-editor** tag would not work.  You'll need to create this web component with React in the next few steps.  The **main.js** and **main.css** files will contain the React build output for the hero editor tag.

And finally, you will create the **character.html** template.  This template simply allows the user to view the attributes of the hero that they have created.  Open **hero/templates/hero/character.html** and place the following there.

```
{% extends "hero/base.html" %}
{% load static %}

{% block content %}
<div class="d-flex justify-content-center align-items-center vh-100">
    <div class="w-75 p-4 mb-4 border">
        <table class="table">
            <thead>
                <tr>
                    <th>Attribute</th>
                    <th>Value</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td>Strength</td>
                    <td>{{ character.strength }}</td>
                </tr>
                <tr>
                    <td>Dexterity</td>
                    <td>{{ character.dexterity }}</td>
                </tr>
                <tr>
                    <td>Health</td>
                    <td>{{ character.health }}</td>
                </tr>
                <tr>
                    <td>Intelligence</td>
                    <td>{{ character.intelligence }}</td>
                </tr>
                <tr>
                    <td>Charisma</td>
                    <td>{{ character.charisma }}</td>
                </tr>
            </tbody>
        </table>
    </div>
</div>
{% endblock %}
```

### Setting up the React Project

In this section you'll set up the React project and create a web component for use in the Django template.

First create the directory that you'll use to contain the project.

```
mkdir hero-attribute-editor
```

Now create a **package.json** to list the project dependencies and build commands.  

```json
{
  "name": "hero-attribute-editor",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@r2wc/react-to-web-component": "^2.0.2",
    "rc-slider": "^10.2.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@babel/core": "^7.22.10",
    "@babel/preset-env": "^7.22.10",
    "@babel/preset-react": "^7.22.5",
    "babel-loader": "^9.1.3",
    "css-loader": "^6.8.1",
    "style-loader": "^3.3.3",
    "webpack-cli": "^5.1.4"
  }
}
```

The most important dependency here is the **r2wc/react-to-web-component** library.  This library acts as the bridge between a React component and the [Web Component API](https://developer.mozilla.org/en-US/docs/Web/API/Web_components).  The concept of React props, for example, requires special handling to make possible when working in the web component medium.

Next create the **index.js** that is the entrypoint defined in the **package.json** file.  The **index.js** file should contain the following.

```javascript
import React from 'react';

import r2wc from '@r2wc/react-to-web-component';
import AttributeEditor from './components/AttributeEditor';

const wcAttributeEditor = r2wc(AttributeEditor, { props: {} });

customElements.define("hero-attribute-editor", wcAttributeEditor);
```

The **r2wc** library takes the **AttributeEditor** component and wraps it to create a Web Component compatible object.  You then register that web component with the browser using the **customElements** function.  The [customElements](https://developer.mozilla.org/en-US/docs/Web/API/Window/customElements) function is part of the Web Components API.

The next file implements the **AttributeEditor** component using React.  The component renders a series of sliders representing each character attribute.  Each attribute can have up to 100 points assigned to it, but there are only 100 total points available.  When all points are used, any additional points assigned to an attribute will be automatically deducted from other attributes.

The component will issue a custom event on every attribute change.  We'll see later how those events are intercepted in the Django template.

Place the following in the **components/AttributeEditor.jsx** file.

```jsx
import React, { useState } from "react";

import Slider from "rc-slider";
import "rc-slider/assets/index.css";

import "./AttributeEditor.css";

const TOTAL_POINTS = 100;

function AttributeEditor() {
  const [attributes, setAttributes] = useState({
    Strength: 20,
    Dexterity: 20,
    Health: 20,
    Intelligence: 20,
    Charisma: 20,
  });

  const remainingPoints =
    TOTAL_POINTS - Object.values(attributes).reduce((a, b) => a + b, 0);

  const handleSliderChange = (attribute, value) => {
    setAttributes((prev) => {
      const newAttributes = { ...prev, [attribute]: value };
      let totalUsed = Object.values(newAttributes).reduce((a, b) => a + b, 0);

      if (totalUsed > TOTAL_POINTS) {
        let excess = totalUsed - TOTAL_POINTS;
        let attributesToAdjust = Object.keys(newAttributes).filter(
          (key) => key !== attribute
        );

        while (excess > 0) {
          for (let key of attributesToAdjust) {
            if (newAttributes[key] > 0 && excess > 0) {
              newAttributes[key] -= 1;
              excess -= 1;
            }
          }
        }
      }

      window.dispatchEvent(
        new CustomEvent("heroAttributesChange", {
          detail: newAttributes,
          bubbles: true,
          composed: true,
        })
      );

      return newAttributes;
    });
  };

  return (
    <div>
      <div className="total">Total Points Left: {remainingPoints}</div>
      {Object.entries(attributes).map(([attribute, value]) => (
        <div className="attribute" key={attribute}>
          <label htmlFor={attribute}>{attribute}</label>
          <Slider
            value={value}
            min={0}
            max={100}
            onChange={(val) => handleSliderChange(attribute, val)}
          />
        </div>
      ))}
    </div>
  );
}

export default AttributeEditor;
```

The **AttributeEditor** contains a few references to CSS classes.  Create a new file at **components/AttributeEditor.css** and insert the following.

```css
.attribute {
  margin: 65px 0;
}

.attribute label {
  display: block;
  margin-bottom: 10px;
}

.total {
  font-weight: bold;
  text-align: center;
  margin-bottom: 30px;
}
```

Babel requires a config file named **.babelrc**, where you'll define **preset-react** as one of the loaded presets.  The React preset loads a collection of plugins that are capable of transforming JSX into JavaScript for the final build output.

```json
{
  "presets": [
    "@babel/preset-env", 
    ["@babel/preset-react", {"runtime": "automatic"}]
  ]
}
```
 
Finally, you'll create a **webpack.config.js** file to instruct Webpack how to build the project.

The config defines **index.js** as the build entrypoint, and sets up Webpack to compile any JSX or CSS files that it finds.  The config also strips the React runtime out of the compiled output, as the Django template will use a script tag to download the React runtime from a CDN.

```javascript
const path = require('path');

module.exports = {
  entry: './index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.js',
    libraryTarget: 'umd'
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.jsx']
  },
  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM'
  }
};
```

You should now be able to build the React project.  You can do so using an **npm** command.

```
npm run build
```

This should result in a **dist** directory that contains an **index.js** build file.  The entire React component and its CSS are compiled into this file.

Now you should copy this output file into the appropriate Django static directory.

```
cp dist/index.js ../hero_builder/hero/static/hero/js/main.js
```

Start the Django dev server if it isn't already running and visit **http://localhost:8000** again to view the app.

<img src="https://circumeo.io/static/images/hero_builder.png" style="width: 100%"/>

### Intercepting Custom Events in the Django Template

The final piece of the puzzle is gathering the data from the **hero-attribute-editor** and POSTing it to Django.

Django doesn't know anything about the web component, so you'll need to construct a hidden form to POST the data.  You'll also use a small amount of JavaScript to update the hidden form inputs whenever the custom event is received.

Open the **hero_builder/hero/templates/hero/home.html** template and update it with the following.

```
{% extends "hero/base.html" %}
{% load static %}

{% block extra_js %}
<script src="{% static 'hero/js/main.js' %}"></script>
{% endblock %}

{% block extra_css %}
<link href="{% static 'hero/css/main.css' %}" rel="stylesheet">
{% endblock %}

{% block content %}
<div class="d-flex justify-content-center align-items-center vh-100">
    <div class="w-75 p-4 mb-4 border">
        <hero-attribute-editor></hero-attribute-editor>
        <form method="POST" action="{% url 'character' %}">
            {% csrf_token %}
            
            {{ form.strength.as_hidden }}
            {{ form.dexterity.as_hidden }}
            {{ form.health.as_hidden }}
            {{ form.intelligence.as_hidden }}
            {{ form.charisma.as_hidden }}

            <button type="submit" class="btn btn-primary">Submit</button>
        </form>
    </div>
</div>
<script>
const attributes = [
    "Strength",
    "Dexterity",
    "Health",
    "Intelligence",
    "Charisma"
];

window.addEventListener("heroAttributesChange", (e) => {
    for (const attribute of attributes) {
        const lowercasedAttribute = attribute.toLowerCase();
        const value = e.detail[attribute];

        const inputElement = document.getElementById(`id_${lowercasedAttribute}`);
        if (inputElement) {
            inputElement.value = value;
        }
    }
});
</script>
{% endblock %}
```

The final script tag in the template will listen for **heroAttributesChange** events from the web component and update the hidden form.  On form submission, Django will handle the form as it would any other form.

### GitHub Repositories

You can find the code for the [Django application on GitHub here](https://github.com/zchtodd/hero-builder-tutorial).  The code for the hero attribute editor component [can also be found on GitHub](https://github.com/zchtodd/hero-attribute-editor).

### Conclusion

You should now have a working example of including a React-based web component in a Django application.  This approach still uses template rendering with Django, but allows for progressive enhancement where the requirements demand more interactivity.