Recipe: Django + Micra.js

Django renders the HTML, Micra makes parts of it reactive. The two don’t collide: Micra’s directives are data-* attributes and @event attributes, and it has no {{ }} syntax of its own, so Django templates need no {% verbatim %} blocks.

There are three Django-specific details, and all three are covered below: the CSRF header name, how booleans render in templates, and how to pass structured data.

1. Load Micra in the base template

{# templates/base.html #}
{% load static %}
<!DOCTYPE html>
<html>
  <head>
    <meta name="csrf-token" content="{{ csrf_token }}">
    <script src="https://cdn.jsdelivr.net/npm/micra.js@2/dist/micra.min.js" defer></script>
    <script src="{% static 'js/components.js' %}" defer></script>
  </head>
  <body>
    {% block content %}{% endblock %}
  </body>
</html>
// static/js/components.js
Micra.define('like', { /* … */ })
Micra.start()

Both scripts are defer, so they run in order after the HTML is parsed. Prefer to serve Micra yourself? Download micra.min.js into static/js/ and use {% static %} for it too.

2. CSRF: one setting

this.fetch() reads the token from <meta name="csrf-token"> and sends it in the X-CSRF-Token header on same-origin requests. That’s the Rails and Laravel convention. Django looks for a different header by default, X-CSRFToken, so out of the box every POST from Micra gets a 403.

Point Django at the header Micra sends:

# settings.py
CSRF_HEADER_NAME = "HTTP_X_CSRF_TOKEN"

That’s the whole integration. The meta tag from step 1 supplies the token: rendering {{ csrf_token }} also makes Django set the csrftoken cookie that the header is checked against.

The setting renames the header for the whole project. If other code already sends X-CSRFToken (a common htmx setup is hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' on <body>), switch that code to X-CSRF-Token too, or leave the setting alone and use the per-call header below.

If you’d rather not change the setting, pass Django’s header yourself on each call. Read it once in onCreate and keep it on the instance:

Micra.define('like', {
  state: { likes: 0 },

  onCreate() {
    this._csrf = document.querySelector('meta[name="csrf-token"]').content
  },

  async toggle() {
    await this.fetch('/posts/42/like/', {
      method: 'POST',
      body: { liked: true },
      headers: { 'X-CSRFToken': this._csrf },
    })
  },
})

3. Props from templates: booleans need yesno

this.prop() turns the attribute strings "true" and "false" into booleans and numeric strings into numbers. Django renders Python booleans capitalized:

<div data-component="like" data-liked="{{ post.liked }}">
{# renders data-liked="True" — prop('liked', false) returns the string "True" #}

The string "True" is truthy, and so is "False". Render booleans through yesno:

<div data-component="like"
     data-post-id="{{ post.pk }}"
     data-liked="{{ post.liked|yesno:'true,false' }}"
     data-likes="{{ post.likes }}">

Numbers ({{ post.likes }}, {{ post.pk }}) need nothing extra.

4. Structured data: json_script

For a list or a nested object, don’t build JSON inside an attribute. Use Django’s json_script filter, which writes a <script type="application/json"> tag and escapes everything that could close it early:

<div data-component="task-list">
  <template data-each="tasks" data-key="id">
    <label>
      <input type="checkbox" data-bind="checked:item.done" @change="toggle(item.id)">
      <span data-text="item.title"></span>
    </label>
  </template>
</div>
{{ tasks|json_script:"tasks-data" }}
Micra.define('task-list', {
  state: { tasks: [] },

  onCreate() {
    this.state.tasks = JSON.parse(document.getElementById('tasks-data').textContent)
  },

  toggle(id) {
    this.state.tasks = this.state.tasks.map(t =>
      t.id === id ? { ...t, done: !t.done } : t
    )
  },
})

In the view, pass a list of plain dicts, for example list(Task.objects.values('id', 'title', 'done')).

5. A JSON view for the component

this.fetch() sends the body as JSON and sets Content-Type: application/json, so the view reads request.body, not request.POST:

# views.py
import json
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.http import require_POST

from .models import Post

@require_POST
def like(request, pk):
    post = get_object_or_404(Post, pk=pk)
    liked = json.loads(request.body)["liked"]
    # … record the like for request.user …
    return JsonResponse({"likes": post.likes})
# urls.py
path("posts/<int:pk>/like/", views.like, name="post-like"),

this.fetch() returns the parsed JSON on a 2xx response and throws a FetchError (with .status) otherwise. A 403 there almost always means the CSRF header: check step 2.

The client side of this exact like button, with an optimistic update and a rollback, is the optimistic updates recipe.

With htmx

Django projects often use htmx (and django-htmx) for server round-trips. Micra sits alongside it for client-only state. See the htmx recipe for the three-line bridge that mounts and tears down components as htmx swaps fragments.