How to setup Django with React using InertiaJS

The official Django Inertia adapter was released in December 2022 but there was 0 front-end documentation and only the Django part of documentation - even now (as of 2nd February 2025) it says "Django specific frontend docs coming soon." with references to 2 other repos from where we need to pull our hair to try to get it to work. I finally landed on Mujahid Anuar's repo which was for Vue but managed to port it to React with bits and pieces from StackOverflow, Claude.ai and online documentation.

This is not a tutorial on Django or React - this article shows how to bind React in Django using Inertia instead of using API endpoints using DRF. So I am cutting short adding models in Django etc to delve directly into the front-end usage sending hardcoded props to React code.

cd workspace/django
mkdir inertia-django-vite-react-minimal
cd inertia-django-vite-react-minimal
python3 -m venv venv
source venv/bin/activate
pip install django==5.1.5
django-admin startproject inertia_django_vite_react_minimal .
pip install django-vite==3.0.6 inertia-django==1.1.0 whitenoise==6.8.2
python manage.py startapp app

Install Node if you haven't already have - minimum version required is version 22.0.0

touch package.json
code .

Add this to package.json :

{
    "scripts": {
        "dev": "vite",
        "build": "vite build"
    },
    "devDependencies": {
        "vite": "^6.0.11"
    },
    "dependencies": {
        "@inertiajs/progress": "^0.2.7",
        "@inertiajs/react": "^2.0.3",
        "@types/node": "^22.10.10",
        "@vitejs/plugin-react": "^4.3.4",
        "react": "^19.0.0",
        "react-dom": "^19.0.0"
    }
}
npm install

If you get something like this because you already have an older NodeJS installed, either re-install NodeJS version 22.0 or later or use nvm.

npm warn EBADENGINE Unsupported engine {
npm warn EBADENGINE   package: 'vite@6.0.11',
npm warn EBADENGINE   required: { node: '^18.0.0 || ^20.0.0 || >=22.0.0' },
npm warn EBADENGINE   current: { node: 'v21.7.3', npm: '10.9.0' }
npm warn EBADENGINE }
npm warn deprecated lodash.isequal@4.5.0: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.

I have nvm installed so I had to do nvm install 22

touch vite.config.ts

Add this to vite.config.ts

import { defineConfig } from "vite";
import { resolve } from "path";
import react from "@vitejs/plugin-react";

export default defineConfig({
  root: resolve("./app/static/src"),
  base: "/static/",
  plugins: [react()],
  build: {
    outDir: resolve("./app/static/dist"),
    assetsDir: "",
    manifest: "manifest.json",
    emptyOutDir: true,
    rollupOptions: {
      // Overwrite default .html entry to main.tsx in the static directory
      input: resolve("./app/static/src/main.tsx"),
    },
  },
});
python manage.py migrate
python manage.py runserver
# Open a new tab in the terminal in the same project directory and run
npm run dev

In inertia_django_vite_react_minimal/urls.py you should have URLs of app

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path("", include("app.urls")), # Include app's URLs 
]

Create a file called urls.py in the app folder.

In app/urls.py let's create the home and about URLs:

from django.urls import path

from . import views

urlpatterns = [
    path("", views.index, name="index"),
    path("about", views.about, name="about"),
]

And in the views.py :

from django.http import HttpRequest
from django.shortcuts import render
from inertia import render as inertia_render # So that we can keep using django's default render() for non Inertia/React pages
from time import sleep

def index(request):
    return inertia_render(request, "Index", props={"name": "World"})


def about(request):
    sleep(2.5) # This is to show the loading progress indicator on the front-end using the @inertiajs/progress package
    return inertia_render(request, "About", props={"pageName": "About"})

Create a folder called templates in the app folder.

In the templates folder create file called index.html paste this content in it :

{% load django_vite %}
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="csrf-token" content="{{ csrf_token }}">

    {% if debug %}
    <script type="module">
    import RefreshRuntime from 'http://localhost:5173/static/@react-refresh'
    RefreshRuntime.injectIntoGlobalHook(window)
    window.$RefreshReg$ = () => {}
    window.$RefreshSig$ = () => (type) => type
    window.__vite_plugin_react_preamble_installed__ = true
    </script>
    {% endif %}

  {% vite_hmr_client %}
  {% vite_asset 'main.tsx' %}

  <title>Inertia + Django + Vite + React minimal</title>
</head>

<body>
  {% block inertia %}{% endblock %}
</body>

</html>

The http://localhost:5173/static/@react-refresh is for hot reload in development mode - when DEBUG is true is in settings.py - when you want the front-end to reload automatically on changes in the React files. Reference: https://stackoverflow.com/a/77971927/126833

Create a folder called static in the app folder and create a sub-folder called src In src create a file called main.tsx and have this in it :

import "vite/modulepreload-polyfill";
import { createRoot } from "react-dom/client";
import { createInertiaApp } from "@inertiajs/react";
import { InertiaProgress } from '@inertiajs/progress';
import axios from 'axios';
import { Page } from "@inertiajs/core";
import React from 'react';  // Added this import
document.addEventListener('DOMContentLoaded', () => {

    const csrfToken = document.querySelector('meta[name=csrf-token]').content;
    axios.defaults.headers.common['X-CSRF-Token'] = csrfToken;

    InertiaProgress.init();

    createInertiaApp({
        resolve: (name) => import(`./pages/${name}.tsx`),
        setup({ el, App, props }: {
            el: HTMLElement,
            App: React.ComponentType<{ page: Page }>,
            props: any
        }) {
            const root = createRoot(el);
            root.render(<App {...props} />);
        },
    });

});

Create 2 files in the app/static/src/pages folder - one named Index.tsx and the other named About.tsx - these are pure React code in TypeScript.

Index.tsx

import React from 'react';
import { Link, usePage } from '@inertiajs/react';

export default function Index() {

  const { app_name } = usePage().props;

  return (
      <div>
          <h1>{app_name}</h1>
          <h1>Welcome to the Home Page</h1>
          <Link href="/about">About</Link>
      </div>
  );
}

About.tsx

import React from 'react';
import {Link, usePage} from '@inertiajs/react';

export default function About() {

  const { app_name } = usePage().props;

  return (
      <div>
          <h1>{app_name}</h1>
          <h1>About Page</h1>
          <Link href="/">Back to Home</Link>
      </div>
  );
}

Create a folder in app called middleware and in middleware create a file called mInertia.py (I purposely didn't want to name it inertia.py incase of conflicts due to the existing file of the same name in packages)

app/middleware/mInertia.py

from inertia import share
from django.conf import settings
from django.contrib.auth import get_user_model


class InertiaShareMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # Share data that should be available to all components
        share(
            request,
            app_name=settings.APP_NAME, # This is set in settings.py
            user=lambda: self._get_user_data(request),
            user_count=lambda: get_user_model().objects.count(),
            # Add more shared data as needed
        )

        return self.get_response(request)

    def _get_user_data(self, request):
        """Format user data for frontend components"""
        if request.user.is_authenticated:
            return {
                'id': request.user.id,
                'email': request.user.email,
                'name': request.user.get_full_name(),
                'is_staff': request.user.is_staff,
            }
        return None

Create a file called context_processors.py in inertia_django_vite_react_minimal - this is to send the DEBUG value with the keyname debug to the front-end template to use as {% if debug %} ... {% endif %}

inertia_django_vite_react_minimal/context_processors.py :

from django.conf import settings

def debug_mode(request):
    return {'debug': settings.DEBUG}

There a number of edits in settings.py (inertia_django_vite_react_minimal/settings.py) :

import os
import re
.
.
.
DEBUG = True
APP_NAME = "Django with Inertia using React" # This was added
ALLOWED_HOSTS = ["*"]

# Application definition

INSTALLED_APPS = [
    'whitenoise.runserver_nostatic', # This was added
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_vite', # This was added
    'inertia', # This was added
    'app', # This was added
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'inertia.middleware.InertiaMiddleware', # This was added
    'app.middleware.mInertia.InertiaShareMiddleware', # This was added
]
.
.
.
# django-vite settings
# https://github.com/MrBin99/django-vite
DJANGO_VITE_DEV_MODE = DEBUG  # DEBUG - follow Django's dev mode

# Where ViteJS assets are built.
DJANGO_VITE_ASSETS_PATH = BASE_DIR / "app" / "static" / "dist"

# Vite 3 defaults to 5173. Default for django-vite is 3000, which is the default for Vite 2.
DJANGO_VITE_DEV_SERVER_PORT = 5173

# Output directory for collectstatic to put all your static files into.
STATIC_ROOT = BASE_DIR / "staticfiles"

DJANGO_VITE_MANIFEST_PATH = os.path.join(STATIC_ROOT, "manifest.json")

# Include DJANGO_VITE_ASSETS_PATH into STATICFILES_DIRS to be copied inside
# when run command python manage.py collectstatic
STATICFILES_DIRS = [DJANGO_VITE_ASSETS_PATH]

# Inertia settings
INERTIA_LAYOUT = BASE_DIR / "app" / "templates/index.html"

# Vite generates files with 8 hash digits
# http://whitenoise.evans.io/en/stable/django.html#WHITENOISE_IMMUTABLE_FILE_TEST
def immutable_file_test(path, url):
    # Match filename with 12 hex digits before the extension
    # e.g. app.db8f2edc0c8a.js
    return re.match(r"^.+\.[0-9a-f]{8,12}\..+$", url)

Now when you goto http://localhost:8000/ (I’m assuming that python manage.py runserver and npm run dev are already running) you should see this which is served by app/static/src/Pages/Index.tsx :

And on clicking About, you should be able to see the About Page page loading in 2 and half seconds with a blue progress indicator showing at the top of the page which is processed by @inertiajs/progress which we added in package.json.

GitHub repo : https://github.com/anjanesh/inertia-django-vite-react-minimal/

0
Subscribe to my newsletter

Read articles from Anjanesh Lekshminarayanan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Anjanesh Lekshminarayanan
Anjanesh Lekshminarayanan

I am a web developer from Navi Mumbai working as a consultant for NuSummit (formerly cloudxchange.io). Mainly dealt with LAMP stack, now into Django and trying to learn Laravel and Google Cloud. TensorFlow in the near future. Founder of nerul.in