Mixins — compose your own state¶
The CRUD classes in reflex-django (ModelState, ModelCRUDView) are themselves built out of small mixins. Each mixin adds one piece of behavior: list rows, create rows, delete rows, scope to current user, enforce a permission, paginate, validate.
If your page needs something different from the standard CRUD shape — say, "list and create but never edit or delete", or "permission-checked admin tool with custom save logic" — you can compose only the mixins you need.
This page is a tour of the mixins and a few recipes.
When you'd want this¶
- You want fewer capabilities than
ModelStateprovides (e.g. a list page that only lets you add, not edit). - You want more capabilities (e.g. add a
LoginRequiredMixinto every CRUD action). - You want to build your own variant of
ModelCRUDViewfor your project's conventions.
If your page is a standard "list + edit + save + delete", stick with ModelState. Mixins are a step deeper than most projects need.
How the standard CRUD class is built¶
ModelCRUDView is just this composition, in MRO order:
ModelCRUDView
├── DispatchMixin # the run-loop that calls hooks
├── DeleteMixin # delete handler + before/after_delete
├── UpdateMixin # update handler + before/after_save
├── CreateMixin # create handler + get_create_kwargs
├── ListMixin # list / refresh / filter / paginate
├── StateFieldsMixin # auto-declared reactive vars from `fields`
├── PermissionMixin # @login_required / @permission_required hooks
├── QuerysetMixin # get_queryset / filter_queryset
├── SerializeMixin # serializer wiring
└── ObjectMixin # get_object_lookup
Drop any of these and you get a smaller, more specialized state class.
The mixin catalog¶
BaseModelState¶
The minimum a CRUD-ish state needs: model, class Meta, dispatch(). Almost never used directly — it's the bottom of the stack.
QuerysetMixin¶
Provides get_queryset() and filter_queryset(qs). Override both to control which rows the state can see.
def get_queryset(self):
return Order.objects.filter(tenant=self.request.user.tenant)
def filter_queryset(self, qs):
qs = super().filter_queryset(qs)
if self.only_recent:
qs = qs.filter(created_at__gte=...)
return qs
ObjectMixin¶
Provides get_object_lookup(pk). This is the "fetch one row by ID" path used by edit/delete. Override to add an ownership filter:
SerializeMixin¶
Wires a ReflexDjangoModelSerializer (either auto-built from fields or your serializer_class).
StateFieldsMixin¶
At class-definition time, auto-declares one reactive variable per entry in fields (and a matching set_<field> setter).
ListMixin¶
Provides:
refresh()— reload the list with current filter/search/pagefilter()— re-apply searchclear_filter()— reset searchpaginate(page)/next_page()/prev_page()(whenpaginate_by > 0)
CreateMixin¶
Provides:
create()— enter "new row" mode (clears the form)- The create branch of
save()— used byUpdateMixin.save. - Hooks:
get_create_kwargs(state_data),before_save(instance),after_save(instance)
UpdateMixin¶
Provides:
save()— validate + create-or-update + reloadstart_editing(pk)/cancel_edit()- Hooks:
before_save,after_save,clean_<field>,validate_state
DeleteMixin¶
Provides:
delete(pk)— fetch + delete + reload- Hooks:
before_delete(instance),after_delete(instance)
DispatchMixin¶
The orchestrator. When a handler is called, it routes through:
bind_request_context()— exposesself.request,self.request.user- Permission checks (from
PermissionMixin) - Validation hooks
- Database operation
- State variable updates
- Browser diff
PermissionMixin¶
Wires DRF-style permission classes:
Permission classes are checked on every dispatch before the operation runs.
LoginRequiredMixin¶
Enforces login on every dispatched action. If anonymous, redirects to REFLEX_DJANGO_LOGIN_URL.
UserScopedMixin¶
The "scope to current user" mixin. Replaces three hooks (get_queryset, get_object_lookup, get_create_kwargs) with sensible defaults driven by Meta.owner_field.
class TodoState(UserScopedMixin, ModelState):
model = Todo
fields = ["title", "done"]
class Meta:
list_var = "todos"
owner_field = "owner" # the FK field on Todo
PaginationMixin¶
Provides the pagination variables and handlers when Meta.paginate_by > 0. Already included in ModelState/ModelCRUDView.
OrmApiMixin¶
Provides the low-level async ORM wrappers used by Create/Update/Delete. You almost never override these directly.
Recipes¶
List + create, no edit, no delete¶
For an "append-only" log:
from reflex_django.state import (
BaseModelState, DispatchMixin, CreateMixin, ListMixin,
StateFieldsMixin, SerializeMixin, QuerysetMixin, ObjectMixin,
PermissionMixin,
)
from reflex_django.state import AppState
class AuditLogState(
AppState,
DispatchMixin,
CreateMixin,
ListMixin,
StateFieldsMixin,
PermissionMixin,
QuerysetMixin,
SerializeMixin,
ObjectMixin,
BaseModelState,
):
model = AuditLog
fields = ["action", "details"]
class Meta:
list_var = "events"
No UpdateMixin, no DeleteMixin → those handlers don't exist on the class. Users physically can't call them from the UI.
Read-only list¶
Use ModelListView directly (it's already composed this way):
from reflex_django.state import ModelListView
class CatalogState(ModelListView):
model = Product
fields = ["name", "price"]
class Meta:
list_var = "products"
paginate_by = 25
Login required + per-user scope¶
from reflex_django.mixins import LoginRequiredMixin, UserScopedMixin
from reflex_django.state import ModelState
class NotesState(LoginRequiredMixin, UserScopedMixin, ModelState):
model = Note
fields = ["title", "body"]
class Meta:
list_var = "notes"
owner_field = "owner"
Order matters: LoginRequiredMixin and UserScopedMixin go before ModelState. The MRO walks left to right, so the auth and scoping checks run before the standard CRUD machinery.
DRF-style permission classes¶
from reflex_django.permissions import IsAuthenticated, IsStaff
class AdminToolState(AppState, ModelCRUDView):
model = Tool
serializer_class = ToolSerializer
class Meta:
list_var = "tools"
permission_classes = (IsAuthenticated, IsStaff)
Permission classes are called with (state, action_name) on every dispatch and must return True. If any returns False, the dispatch is rejected.
Building your own base class¶
If most of your CRUD pages need login + user-scoping, factor it out:
# myapp/state_bases.py
from reflex_django.mixins import LoginRequiredMixin, UserScopedMixin
from reflex_django.state import ModelState
class AuthScopedModelState(LoginRequiredMixin, UserScopedMixin, ModelState):
"""Project-wide CRUD base: requires login, scopes to current user."""
class Meta:
abstract = True
owner_field = "owner"
# blog/views.py
class PostState(AuthScopedModelState):
model = Post
fields = ["title", "content"]
class Meta(AuthScopedModelState.Meta):
list_var = "posts"
Session-login mixin (for canned auth pages)¶
The built-in login page uses a special mixin called session_auth_mixin:
from reflex_django.mixins import session_auth_mixin, SessionAuthConfig
from reflex_django import DjangoUserState
config = SessionAuthConfig(
login_url="/login",
post_login_url="/",
post_logout_url="/login",
username_field="username",
min_password_length=8,
)
AuthBase = session_auth_mixin(config, base=DjangoUserState)
class MyAuthState(AuthBase):
"""Now has .login(), .logout(), .register() methods."""
You don't usually need this directly — add_auth_pages() wires it up for you. It's here for projects building a custom auth UI on top of the same primitives.
When not to bother with mixins¶
Honestly: most of the time. ModelState covers 90% of cases with no composition required. Reach for mixins when:
- You explicitly want fewer capabilities (security via removed methods).
- You want to centralize a project-wide pattern (login + scope, audit log, etc.).
- You want to share behavior across CRUD and non-CRUD states.
If you're starting your first reflex-django project, skip this page and come back when you actually feel the need.
Next: Forms & validation →