Django and React - How to use InertiaJS in your Django and React Apps

When building an app with Django and React the conventional wisdom suggests building an API using Django Rest Framework, and then layering React on top as a Single Page Application (SPA). While this approach is certainly effective, it’s not the only path you can take.

In this high-level tutorial, we will explore a simpler, yet equally powerful alternative: the combination of Django, React, and InertiaJS. This makes it easier to use React in Django and reduces complexity without sacrificing functionality.

What is InertiaJS

Inertia allows you to create fully client-side rendered, single-page apps, without the complexity that comes with modern SPAs. It does this by leveraging existing server-side patterns that you already love.

In plain english? Inertia connects your fancy frontend (React, Svelte, or Vue) frontend with your Django backend.

This means you can keep all the good things that Django already offers, authentication, permissions, python πŸ˜ƒ etc and also get that fast, beautiful UI without building an API, if you don’t want to.

How does InertiaJS this work?

Inertia replaces server-side templates with JavaScript components. It acts as a client-side router for smoother page transitions.

  • Page visits are intercepted by Inertia and forwarded to the server via XHR.

  • The server responds to Inertia visits with a JSON response, not a full HTML response.

  • Inertia dynamically updates the client-side page and the browser's history state.

Technical details aka β€œThe protocol”

InertiaJS is not a framework. It is a protocol. It specifies how requests and responses should be made between a client and a server. On the client side, there is a router for handling requests and responses, the backend is configured, (usually via middleware) to respond with the appropriate response.

Inertia.js Protocol Summary

  1. HTML Responses : The initial request to an Inertia app is a full-page browser request. The server returns a full HTML document with a root <div> that includes a JSON-encoded page object for the initial page.

  2. Inertia Responses : Subsequent requests are made via XHR with a X-Inertia header. The server responds with a JSON-encoded page object instead of a full HTML document.

  3. Page Object : Inertia uses a page object to share data between the server and the client. It includes the component name, props (data), URL, and current asset version.

  4. Asset Versioning : Inertia tracks the current version of the site’s assets. If an asset changes, Inertia makes a full-page visit instead of an XHR visit.

  5. Partial Reloads : Inertia allows requesting a subset of the props from the server on subsequent visits to the same page component for performance optimization.

In future posts, we will dive deeper and write our custom middleware. You can find the more technical bits here .

A Deep dive - The Inertia decorator

To understand how InertiaJS works, we can look at the view decorator and how it processes requests.

Decorators

In Python, a decorator is a design pattern that allows a user to add new functionality to an existing object without modifying its structure. Decorators are usually called before the definition of a function you want to decorate. Here are some important points about decorators:

  • Decorators provide a simple syntax for calling higher-order functions.

  • A Python decorator is a specific change to the Python syntax that allows us to more conveniently alter functions and methods (and possibly classes in a future version).

  • This supports more readable applications of the DecoratorPattern but also other uses as well.

  • It’s a function that takes another function and extends the behavior of the latter function without explicitly modifying it.

In Django, decorators are used in various places, for example, to decorate view functions to restrict access to certain views based on conditions, or to pass some arguments based on conditions.

Here is an example of a simple Python decorator:

text
 def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

# Output:
# Something is happening before the function is called.
# Hello!
# Something is happening after the function is called.

In the above example, @my_decorator is used to β€œdecorate” the say_hello function. The decorator function my_decorator takes a function as an argument, defines a new function wrapper that calls the original function with some additional behavior, and returns this new function.

A typical Django inertia view looks something like the one below.

python

@inertia('component/path/typically-no-extension')
def some_view(request):
    return {'key':'some value'}

# the decorator is this:
def inertia(component, layout=None, ssr_enabled=False):
    def decorator(func):
        @wraps(func)
        def inner(request, *args, **kwargs):
            props = func(request, *args, **kwargs) # <- view function is run first
            if not isinstance(props, dict):
                return props
            # we call the 'render' function. This is where the magic happens.
            return render(
                request, component, props, layout=layout, ssr_enabled=ssr_enabled
            )

        return inner

    return decorator
  • At first glance, the view is returning a dictionary, but that is not the end of the story.

  • The decorator processes the value that is returned from the view. Notice how the view function is run first.

  • the render function is not the Django render shortcut. This is a custom render function that adheres to the Inertia protocol.

Let’s look at the inertia render function in detail.

python

def render(request, component, props={}, template_data={}):
  def is_a_partial_render():
    """
    Partial renders
    """
    return 'X-Inertia-Partial-Data' in request.headers and request.headers.get('X-Inertia-Partial-Component', '') == component

  def partial_keys():
    return request.headers.get('X-Inertia-Partial-Data', '').split(',')

  def deep_transform_callables(prop):
    ...
    return prop

  def build_props():
    ...
    return deep_transform_callables(_props)

  def render_ssr():
     """
      If ssr is enabled, render the server side render the page.
     """
    data = json_encode(page_data(), cls=settings.INERTIA_JSON_ENCODER)
    # this is a post request to a nodejs process that is running.
    response = requests.post(
      f"{settings.INERTIA_SSR_URL}/render",
      data=data,
      headers={"Content-Type": "application/json"},
    )
    response.raise_for_status()
    return base_render(request, 'inertia_ssr.html', {
     'inertia_layout': settings.INERTIA_LAYOUT,
      **response.json()
    })

  def page_data():
    """
    Returns the data that will be stored injected into the page 
    """
    return {
      'component': component,
      'props': build_props(),
      'url': request.build_absolute_uri(),
      'version': settings.INERTIA_VERSION,
    }

  # if this is an inertia request only return the JSON data.
  # the inertiajs is already loaded in the frontend.  
  if 'X-Inertia' in request.headers:
    return JsonResponse(
      data=page_data(),
      headers={
        'Vary': 'Accept',
        'X-Inertia': 'true',
      },
      encoder=settings.INERTIA_JSON_ENCODER,
    )

  # use this if ssr is enabled 
  if settings.INERTIA_SSR_ENABLED:
    try:
      return render_ssr()
    except Exception:
      pass
  
  # if inertiajs is not loaded(all initial requests)
  # render a normal django template (shell) + embedd page data.
  return base_render(request, 'inertia.html', {
    'inertia_layout': settings.INERTIA_LAYOUT,
    'page': json_encode(page_data(), cls=settings.INERTIA_JSON_ENCODER),
    **template_data,
  })

The render function is a crucial part of integrating Inertia.js with Django and React. It handles the rendering of Inertia.js pages in your Django application and supports both full and partial updates, as well as server-side rendering. Here's a high-level overview of what is happening.

  • Partial Rendering : The function checks if the current request is a partial render, meaning only a portion of the page needs to be updated. This is determined by specific headers in the request.

  • Properties Building : The function builds the properties object that will be passed to the Inertia.js page. It includes global properties those specific to this render. If the render is partial, it only includes the necessary properties.

  • Server-Side Rendering : If server-side rendering is enabled, the function sends a request to the Inertia SSR server with the page data and retrieves the rendered HTML.

  • Page Data Construction : The function constructs the data object for the Inertia.js page, including the component name, the properties, the current URL, and the Inertia version.

  • Response : Depending on the request, the function returns a JSON response with the page data or the rendered HTML.

Building your own InertiaJS - React - Django app

The remainder of this article will focus on how you would go about installing inertia and building an app. The focus is on how everything fits together. In the next article, we will build a full app.

The main components of this stack

  • Backend - Django

  • Frontend Stack

    • Framework - Svelte, Vue React

    • InertiaJS - the client-side router

    • Vite - bundler and dev server

How to structure your application

text
/myproject
|β€”- /apps
|    |β€”- /base
|    |    |β€”- __init__.py
|    |    |β€”- admin.py
|    |    |β€”- apps.py
|    |    |β€”- /migrations
|    |    |β€”- models.py
|    |    |β€”- tests.py
|    |    |β€”- views.py
|    |    |β€”- urls.py
|β€”- /config
|    |β€”- __init__.py
|    |β€”- settings.py
|    |β€”- urls.py
|    |β€”- wsgi.py
|β€”- /frontend
|    |β€”- /src
|    |    |β€”- /pages
|    |    |    |β€”- /base
|    |    |    |    |β€”- components
|    |    |    |    |β€”- page.jsx
|    |    |β€”- app.tsx
|    |β€”- package.json
|    |β€”- vite.config.js
|β€”- manage.py
|β€”- requirements.txt

Setup the Django Backend

The following libraries will make it easier to work with InertiaJS and Vite.

  • Django Vite - ViteJS is a frontend bundler that helps manage dependencies for your frontend app. Run pip install django-vite to add the package.

  • inertia-django - This library provides a few scripts or β€˜adapters’ (namely, Django middleware, a render function (already discussed above), config, and templates) for handling InertiaJS requests. Run pip install inertia-django to add the package.

Add the installed apps to your settings file

python
# Installed Apps

INSTALLED_APPS = [
  # django apps,
  'inertia',
  'django_vite'
  # your project's apps,
]

MIDDLEWARE = [
 # other middleware (security, csrf etc)
  'inertia.middleware.InertiaMiddleware',
  'apps.common.inertia_share.middleware', # this does not come with the library see below
]

INERTIA_LAYOUR = 'layouts/inertia.html'
  • Add both Django Vite and InertiaJS to the installed apps.

  • The Middleware the list includes our inertia share middle. See the implementation below.

Custom middleware

python
from inertia import share
from django.conf import settings
from .models import User

def inertia_share(get_response):
  def middleware(request):
    share(request, 
      app_name=settings.APP_NAME,
      userId=lambda: request.user.id, # evaluated lazily at render time
    )

    return get_response(request)
  return middleware
  • Custom middleware for sharing data that is common across views. These are the global properties that we referred to earlier in the deep dive section.

  • In this case, we include the userId but this could be anything. Just make sure not to overload this with logic that could slow the request down.

Inertia Templates

html
{% extends inertia_layout %}{% load django_vite %}

{% vite_asset 'frontend/src/app.tsx' %} <- entry point. See ViteConfig later
{% block inertia %}
  <div id="app" data-page="{{page|escape}}"></div>
{% endblock inertia %}
  • This is the base layout template. This is the template that is returned on the initial request.

  • <div id="app" data-page="{{page|escape}}"></div> -

    • the app will be mounted on the app root element. This means that a component is created and inserted into the DOM.

    • The data-page attribute includes the data from the backend. Recall (from the Deep Dive) how the render function builds this JSON dict and either injects it into the template.

Creating an inertia view

text
from inertia import lazy, inertia
from .models import Data


@inertia('pages/base/page')
def data_view(request):
  return {
    'data': Data.objects.all()
  }
  • Instead of returning html, this view will return a dictionary.

  • @inertia('pages/base/page') - this is the path of the component, without the extension. This path is relative to where the entry point.

  • For normal requests, the middleware will return the layout template, so that html can be rendered and inject the data context into the page.

Setup the frontend

We need to set up the ViteJS config and the entry point or InertiaJS app on the frontend.

ViteJS Config

Here is an example config file for vite.js. For a detailed tutorial on Django and Vite, you can visit this post .

text
const { resolve } = require('path');
import react from '@vitejs/plugin-react'

module.exports = {
  plugins: [react()],
  root: resolve('./assets'),
  base: '/static/',
  server: {
    host: 'localhost',
    port: 5173, <-- default server for vite
    open: false,
    watch: {
      usePolling: true,
      disableGlobbing: false,
    },
  },
  resolve: {
    extensions: ['.js', '.json', '.jsx', '.ts', '.tsx'],
  },
  build: {
    outDir: resolve('./static/dist'),
    assetsDir: '',
    manifest: true,
    emptyOutDir: true,
    target: 'es2015',
    rollupOptions: {
      input: {
        main: resolve('./frontend/src/app.tsx'), <- entry point
      }
    },
  },
};

InertiaJS Root Component

Run pnpm i @inertiajs/react to install inertia for react.

jsx
import { createInertiaApp } from '@inertiajs/react'
import { createRoot } from 'react-dom/client'

createInertiaApp({
  resolve: name => {
    // `name` will be the path of the component defined in
    const pages = import.meta.glob('./pages/**/*.jsx', { eager: true })
    return pages[`./pages/${name}.jsx`]
  },
  setup({ el, App, props }) {
    createRoot(el).render(<App {...props} />)
  },
})
  • resolve - this is where you import the page. The name is supplied by the backend view. For example

  • the name will be path of the component , relative to app.tsx .

Add the react component

Finally we can add a react component.

jsx
function MyButton() {
  return (
    <button>
      I'm a button
    </button>
  );
}

export default function Page() {
  return (
    <div>
      <h1>Welcome to my app</h1>
      <MyButton />
    </div>
  );
}
  • this page is imported in the resolve section of app.tsx.

Conclusion

In this guide, we explored building applications Django, React, and InertiaJS. We’ve looked at how these can work together to simplify the process of building modern dynamic web applications with Django and React.

A quick recap of what we covered:

  • InertiaJS : We introduced InertiaJS, a client side router and protocol that allows us to create single-page applications using classic server-side routing.

    • How InertiaJS Works : We explained the working mechanism of InertiaJS and how it bridges the gap between server-side and client-side frameworks.

  • Application Structure : We discussed how to structure your application when using these technologies together.

  • Django Middleware and Inertia Decorators : We looked into the Django middleware and Inertia decorators, crucial components in this setup.

  • Building Your Own App : We walked through the steps of building your own application using Django, React, and InertiaJS.

    • Installation : We covered the installation process for all necessary tools and libraries.

    • Inertia Templates and Creating Views : We explained how to create Inertia templates and views in Django.

  • Frontend Setup : We outlined how to set up the frontend using Vite, InertiaJS root, and React.

By leveraging the power of Django, React, and InertiaJS, you can streamline your development process and build efficient, modern web applications. Give it a try in your next project and let me know your thoughts on Twitter/X. @advantchHQ.

Cheers,

Themba.