Skip to content
docs.djust.org

Rendering Dynamic Lists

This guide covers how to render and update dynamic lists efficiently in djust, including keyed diffing with dj-key for identity-stable DOM updates.

Basic List Rendering

Render a list the same way you would in any Django template:

<ul>
  {% for item in items %}
    <li>{{ item.name }}</li>
  {% endfor %}
</ul>

When items changes, djust re-renders the template and sends a minimal VDOM patch to the client. Without keying, elements are matched by their position in the list. This is fine for simple, static-order lists.

Keyed Diffing with dj-key

For lists where items can be reordered, inserted in the middle, or removed, use dj-key to tell the VDOM differ which element represents which logical item. This preserves element state (focus, animations, input values) across updates.

<ul>
  {% for item in items %}
    <li dj-key="{{ item.pk }}">{{ item.name }}</li>
  {% endfor %}
</ul>

dj-key values must be unique among siblings. Use the item's primary key or any stable string identifier.

When to use dj-key

Use dj-key when:

  • Items can be reordered (e.g. drag-and-drop, sorted feeds)
  • Items are inserted or removed from arbitrary positions
  • Each item has interactive state you want preserved (focused inputs, expanded details, etc.)
  • The list can grow large (keyed lookup is O(1) vs O(n) positional matching)

You can skip dj-key for simple append-only lists or lists whose order never changes.

data-key is also accepted

The legacy data-key attribute works identically to dj-key and is retained for compatibility:

<li data-key="{{ item.pk }}">{{ item.name }}</li>

Both dj-key and data-key are explicit opt-ins. The element's id= attribute is not used as a diff key (see MIGRATION.md for the v1.0 change).

Efficient Appending with dj-update

For append-only lists such as chat messages or activity feeds, combine dj-key with dj-update="append" and temporary_assigns to avoid re-sending the entire list on every update:

class ChatView(LiveView):
    def mount(self, request, **kwargs):
        self.messages = Message.objects.order_by('-created_at')[:50]

    def get_context_data(self, **kwargs):
        return {'messages': self.messages}

    @event_handler
    def send_message(self, text: str = "", **kwargs):
        msg = Message.objects.create(text=text, author=request.user)
        self.messages = [msg]  # Only the new message; dj-update="append" adds it

    def temporary_assigns(self):
        return ['messages']   # Reset after each render
<ul id="message-list" dj-update="append">
  {% for msg in messages %}
    <li dj-key="{{ msg.pk }}">{{ msg.text }}</li>
  {% endfor %}
</ul>

dj-update="append" makes the client add new children to the bottom without touching existing ones. temporary_assigns keeps the server-side list small.

Full Example: Sortable Todo List

class TodoView(LiveView):
    def mount(self, request, **kwargs):
        self.todos = list(Todo.objects.order_by('position'))

    @event_handler
    def move_item(self, item_id: int = 0, new_position: int = 0, **kwargs):
        todo = Todo.objects.get(pk=item_id)
        todo.position = new_position
        todo.save()
        self.todos = list(Todo.objects.order_by('position'))
<ul>
  {% for todo in todos %}
    <li dj-key="{{ todo.pk }}"
        dj-click="move_item"
        data-item-id:int="{{ todo.pk }}"
        data-new-position:int="{{ todo.position }}">
      {{ todo.title }}
    </li>
  {% endfor %}
</ul>

With dj-key, the VDOM differ moves existing <li> DOM nodes into their new positions instead of destroying and recreating them, preserving any attached event listeners and element state.

Summary

ScenarioRecommendation
Static order listNo keying needed
Items reordered or inserteddj-key="{{ item.pk }}"
Append-only feeddj-update="append" + temporary_assigns
Legacy data-key usageContinues to work; no migration needed
Old id= implicit key (pre-1.0)Add explicit dj-key= — see MIGRATION.md