How to setup Django with React using InertiaJS
Django 5 + React 19 + Inertia 2 + Vite 6 setup tutorial

Search for a command to run...
Django 5 + React 19 + Inertia 2 + Vite 6 setup tutorial

No comments yet. Be the first to comment.
Clean your email list even before sending it to a mass emailing solution like AutoSend to avoid a high bounce rate

Try using an alternative to openclaw, which they claim is cheaper to use

Using types (and interfaces) to define a blueprint of a variable

Using GraphQL as an alternative to REST API for Hashnode

making makemigrations work in Django when migrations is not pushed to git

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/