Skip to main content

Command Palette

Search for a command to run...

How to setup Django with React using InertiaJS

Django 5 + React 19 + Inertia 2 + Vite 6 setup tutorial

Updated
7 min read
How to setup Django with React using InertiaJS
A
I am a web developer from Navi Mumbai. Mainly dealt with LAMP stack, now into Django and getting into Laravel and Cloud. Founder of nerul.in and gaali.in

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/