Reactive Models
Reactive models in Tetra provide a seamless way to synchronize your database changes with the client-side UI in real-time. By inheriting from ReactiveModel, your models automatically notify subscribed components whenever an instance is saved, created, or deleted.
WebSocket connections are established on-demand
WebSocket connections are only established when a page actually renders a ReactiveComponent. If you have reactive models defined in your codebase but no reactive components are used on a particular page, no WebSocket connection will be initiated.
Overview
When a ReactiveModel is updated on the server:
- A post_save signal triggers a notification.
- If created: component.created is sent to the collection channel (e.g., "demo.todo").
- If updated: component.data_changed is sent to the instance channel (e.g., "demo.todo.236").
- A post_delete signal triggers a component.removed message to both channels.
- Subscribed Tetra ReactiveComponents receive these messages and update their UI automatically.
Usage
To make a model reactive, inherit from tetra.models.ReactiveModel. You should also define which fields are safe to be sent to the client in an inner Tetra class.
from django.db import models
from tetra.models import ReactiveModel
class TodoItem(ReactiveModel):
title = models.CharField(max_length=200)
done = models.BooleanField(default=False)
secret_note = models.CharField(max_length=200)
class Tetra:
# Only these fields will be sent over the WebSocket
fields = ["title", "done", "model_version"]
ReactiveModels automatically include a model_version field to handle concurrency and deduplication of updates.
Configuration with the Tetra class
The inner Tetra class configures the reactive behavior:
fields: A list of field names to be sent to the client on updates.- For security, it defaults to an empty list (only triggering a
_updateHtml()on the client). - Use
"__all__"to send all model fields (use with caution!). - The primary key (
id) is always included.
- For security, it defaults to an empty list (only triggering a
Model Channels
ReactiveModel defines two default channels for each instance:
Instance Channel
Used for updates to a specific record. Default: {app_label}.{model_name}.{pk} (e.g., main.todoitem.1).
Get it via: instance.get_tetra_instance_channel().
Collection Channel
Used for notifications about new or deleted records in a collection. Default: {app_label}.{model_name} (e.g., main.todoitem).
Get it via: instance.get_tetra_collection_channel().
Subscribing to Models
In your ReactiveComponent, you can subscribe to these channels:
Subscribing to a specific instance
class TodoDetail(ReactiveComponent):
def load(self, todo):
self.todo = todo
self.title = todo.title
def get_subscription(self):
return self.todo.get_tetra_instance_channel()
Subscribing to a collection
class TodoList(ReactiveComponent):
subscription = "main.todoitem"
def load(self):
self.todos = TodoItem.objects.all()
TodoItem is created, TodoList will automatically call its _updatHtml() method on all its clients, which re-render the components from the server.
Custom Channels
You can override get_tetra_instance_channel() or get_tetra_collection_channel() on your model to customize the WebSocket group names.
class ProjectTask(ReactiveModel):
project = models.ForeignKey(Project, on_delete=models.CASCADE)
def get_tetra_instance_channel(self):
return f"project.{self.project_id}.task.{self.pk}"
def get_tetra_collection_channel(self):
return f"project.{self.project_id}.tasks"
Security Considerations
By default, ReactiveModel does not send any field data unless explicitly listed in Tetra.fields. This prevents accidental exposure of sensitive data like password hashes or private notes.
Always be selective with the fields you include in Tetra.fields. If you only need to trigger a re-render of a component without sending data, leave fields as an empty list or omit the Tetra class entirely.