DEV Community

Cover image for Using React to Progressively Enhance a Django App
Zach
Zach

Posted on • Originally published at circumeo.io

Using React to Progressively Enhance a Django App

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
Enter fullscreen mode Exit fullscreen mode

Now create a new application within the hero_builder directory.

cd hero_builder
python3 manage.py startapp hero
Enter fullscreen mode Exit fullscreen mode

Run the initial migrations for the SQLite development database.

python3 manage.py migrate
Enter fullscreen mode Exit fullscreen mode

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

python3 manage.py runserver
Enter fullscreen mode Exit fullscreen mode

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

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.

INSTALLED_APPS = [ 
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "hero",
]
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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

python manage.py makemigrations hero
Enter fullscreen mode Exit fullscreen mode

To run the migration:

python manage.py migrate hero
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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.

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})
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

{
  "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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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. 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.

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);
Enter fullscreen mode Exit fullscreen mode

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 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.

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;
Enter fullscreen mode Exit fullscreen mode

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

.attribute {
  margin: 65px 0;
}

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

.total {
  font-weight: bold;
  text-align: center;
  margin-bottom: 30px;
}
Enter fullscreen mode Exit fullscreen mode

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.

{
  "presets": [
    "@babel/preset-env", 
    ["@babel/preset-react", {"runtime": "automatic"}]
  ]
}
Enter fullscreen mode Exit fullscreen mode

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.

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'
  }
};
Enter fullscreen mode Exit fullscreen mode

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

npm run build
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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 %}
Enter fullscreen mode Exit fullscreen mode

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. The code for the hero attribute editor component can also be found on GitHub.

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.

Top comments (0)