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