Back to writing
11 min read

Django Islands: Part 1

On using SolidJS with Django to build a type-safe frontend that the whole team will love.

Django Islands: Part 1
Table of Contents
💡

While this post is about Django, the concepts can be applied to really any backend framework.

Oh, Django

Django is great. The built-in admin app is unreasonably effective. I’ve used it to scale services to millions of users, with many developers working in the same codebase. However, unlike a framework like Next.js, Django leaves the frontend decisions completely up to you. No opinion. No guidance. Largely, Django doesn’t acknowledge JavaScript even exists.

Because of this, I’ve spent way too many hours figuring out how to marry Django with modern JavaScript practices over the years. This post is about my current approach, which I’m calling The Django Island Pattern! This approach will also work with any backend framework that you can wrestle an OpenAPI definition file out of.

Who is this post for?

This post is less of a step-by-step tutorial and more about architecture. There’s a working example repository, but it’s largely to illustrate the concepts and not to use as a boilerplate. This post is for people who are comfortable with Django (or another backend framework) and are interested in adding a scalable, robust, modern JavaScript layer to it.

In other words, this is not intended for beginners.

The many roads to JavaScript

Since Django has no JavaScript opinion, we’re left to figure out our own strategy.

The main ways to integrate JavaScript into Django are as follows:

  • Full JavaScript SPA with Django as an API
  • No or Minimal JavaScript, using a library like HTMX
  • Something in the middle

Full On JavaScript

The full JavaScript approach is becoming more popular, but it feels like throwing up our hands and giving up on Django’s templating system. We’ll end up re-implementing what Django already offers to deliver a large JavaScript bundle to the user. This is bad for the user experience, bad for SEO, and is more work for developers.

No JavaScript

On the other hand, not using JavaScript at all is not really an acceptable practice in today’s product landscape. Users will expect some interactivity. The HTMX library can help fill in the gap here; however, I’ve found it to be fragile in practice. For example, it’s hard to test without running a full browser, which means changing a HTMX feature is error-prone. It also doesn’t cover all the use cases for JavaScript, so we’ll have to use something else eventually anyway.

JavaScript Islands

So what then? Let’s talk about the middle ground: Islands 🏝️. Islands are a relatively new, but useful concept in web development. The main idea is to have a mostly server-side generated page and only activate isolated components that need to be interactive with JavaScript. We’ll call these interactive bits “islands”. You’ll get all the performance and SEO benefits of a server-rendered page while also getting the freedom to meet the user’s expectations on interactivity. Sweet baby Ray.

This approach also scales well. The minimal example here creates a JavaScript bundle around 20kb, 9kb gzipped. That’s much smaller than either just the base React (>100kb) or HTMX (45kb) payloads. Then, as the code grows, you can lazy load your islands on only the pages that need them.

With the definitions out of the way, let’s see what it will take conceptually to make islands with Django.

Architecture Overview

Code Valley

Generally, we want to add interactivity to specific elements on our site. We’d like our islands to work with Django and not have to reimplement a bunch of stuff Django already has, like URL routing. We’d like a good testing story. Ideally, we’ll share type definitions between Django and JavaScript to get the best editor experience. To keep the codebase relatively future-proof, we want to use as many standards as possible. Oh yeah, and if we could get fallbacks for non-JS users and web crawlers, that would be great too, kthx.

To accomplish all of this, we’re going to pull together a few different technologies, with web standards to glue them together. The overall concept should work even if the implementation of each part changes. For example, we’re going to use SolidJS for our JavaScript framework, but it could be another library (or a mix!) no problem because we’ll use Web Components as an interface.

Here’s what a simple request will look like:

  1. User requests a URL.
  2. Django sends back server-rendered HTML with embedded Web Components.
  3. JavaScript binds to the Web Components and mounts the islands.
  4. Optionally, the JavaScript makes additional requests to Django via an API.

We’ll go over how to do all the API stuff in part 2, but we need to set the groundwork first.

Here are the libraries we’ll be using:

Note, because we’re using standards like Web Components and OpenAPI, the implementation details can be replaced with your preferred libraries.

Let’s get to the code! To motivate our examples, we’re going to build a site to help internet sensation Moo Deng figure out what and when to eat.

Example Site: Moo Deng is Hungry!

Moo Deng

Our example site will have two islands, a simple one that doesn’t use the API, and a more complex one to show API usage. Let’s start with the simple component that helps Moo Deng know when it’s time to eat.

The Food Time Component

Feed Time

Moo Deng told me she never knows what time her next meal is. She eats every 3 hours, so she should be able to figure it out, but she’s a tiny, adorable hippo. I’ll give her a pass.

Let’s make a Django Island to tell her how long she has to wait for her next meal.

Why not just do everything server-side?

This feature could serviceably be done server-side. However, an island has several advantages:

  • Easily display the time in the user’s time zone.
  • A countdown can be updated without a browser refresh.
  • It’s an excuse to show a simple island example.

The Django Template Side

The first step is to add a custom element to our Django template. We can name it anything we want, but it’s good practice to make it easy to find with grep. Typically, I’ll give every island a common prefix. For example, w-, which will make our Food Time island <w-food-time>.

Here’s the full component in our Django template:

// food_time.dhtml
<w-food-time time="{{ feed_time|date:'c' }}">
	<time datetime="{{ feed_time|date:'c' }}}">{{ feed_time|date:'U' }}</time>
</w-food-time>

Maybe this is more complicated than you were expecting, but there are a few neat aspects I want to point out. To start, feed_time is a timezone-aware date in UTC of Moo’s next feeding time. In the example, we’re using it as a property on our custom element to pass data to our SolidJS component. We could have stopped there, and most users would get the full experience. However, some users and web crawlers don’t execute JavaScript, so they won’t see anything. For that, we’ll need to do some progressive enhancement.

No JavaScript Fallback

For those cases, we can add a fallback experience which doesn’t require any JavaScript! Users (and bots) without JavaScript will still get the upcoming feeding time formatted in epoch time (U). Wins all around.

When our JavaScript loads and our custom element is mounted, we’ll clear out the children elements to make room for the enhanced experience.

The SolidJS Component

Next, we’ll take a look at the SolidJS component that makes everything work. It’s a standard component that could be used in other SolidJS components:

// Time.jsx
import { createSignal } from "solid-js";
import styles from "./Time.module.css";

export function Time(props: { time: string }) {
  const [now, setNow] = createSignal(Date.now());
  setInterval(() => setNow(Date.now()), 1000);
  const hippoTime = () => {
	const dateTimeFormat = new Intl.DateTimeFormat("en", {
  	year: "numeric", month: "long", day: "numeric", hour: "numeric", minute: "numeric",
	});
	return dateTimeFormat.format(new Date(props.time));
  };
  const secondsToTime = () =>
	Math.floor((new Date(props.time).getTime() - now()) / 1000);
  return (
	<time datetime={props.time} class={styles.Time}>
  	{hippoTime()}, {secondsToTime()} seconds from now!
	</time>
  );
}

This component takes in a prop, time, as a string. It then munges it around a bit in order to render a string like “December 9, 2024 at 2:00 a.m., 10333 seconds from now!” in a time element, while updating every second.

The details here aren’t super important, but I do want to demonstrate how much power islands gives you. On top of that, since this is a normal component, it can be tested and composed like any other SolidJS code, using standard tooling.

The missing piece here is how do we actually mount this component into our site?

The Mounting

Now is where solid-element comes in. Every popular JavaScript view library has something like this, but this one is for SolidJS. This is a popular one for React.

This code uses solid-element to take a SolidJS component, convert it into a Web Component, then mount it on the corresponding custom element.

import { customElement, noShadowDOM } from "solid-element";
import Time from "./Time"; // Our SolidJS component
// "w-food-time" is the custom element we defined in our Django template
// "time" is the default prop value
customElement("w-food-time", { time: "" }, (props, { element }) => {
  noShadowDOM(); // We're not using Shadow DOM, so let's turn it off
  element.innerHTML = ""; // Clear out the existing innerHTML
  return <Time time={props.time} />; // Mount our SolidJS component
});

Here we can see the element tag we used in the Django template, w-food-time. Then we define the default props, which in this case is time. Lastly, we define the function which gets called every time the browser sees an instance of our custom element.

There are a few curiosities here. The first one is this noShadowDOM business. The shadow DOM was supposed to be this holy grail of component reusability nirvana, saving us from style conflicts. However, I’ve found it has too many limitations and browser quirks. I’ve spent enough time arguing with it that I can confidently say you should just turn it off and move on. I know, I’m sad too.

Next we clear out the existing innerHTML. Normally the shadow DOM would do this for us, but, oops, we killed it. You could do other stuff with the existing child elements, like pull data out and feed it to your component, but I’d suggest just using props (or an API call, as we’ll see later).

And that’s pretty much it. You could make a helper utility to reduce boilerplate when making new island components. Or don’t. I’m not your mom.

However, I do want to point out a neat feature of this approach; the browser does all the work finding your custom elements, even after the initial page load. The elements will get mounted on page load, and if you add them programmatically later. Mounting just works as expected. This is great if you want to use something like Turbo to avoid full page refreshes.

What’s next?

We did an island! We’re done! Well, not really. What we made is just the start. Passing simple, static properties to our island will only cover a small number of use cases. To leverage all the power this pattern gives us, we’ll need to load in data dynamically in response to user interaction. We also can’t handle sending data to our backend via forms yet. I’ll cover all of this in part 2.

Conclusion

There are so many cool new ideas from the web development space right now. Sadly, the norm seems to require us to throw out all the great tools we’re used to. If we take a step back, we see how these new ideas can be easily applied to established ecosystems. We don’t have to rewrite the world just to keep up with users’ expectations!

Checkout the example repo.

To see this pattern in action on a production site, check out Totem, our non-profit for finding group support! (Totem is also open source).