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.