On this page
Navigation & URL State¶
djust provides live_patch() and live_redirect() for managing URL state without full page reloads, inspired by Phoenix LiveView. Bookmark-friendly URLs, browser back/forward, and deep linking all work out of the box.
What You Get¶
live_patch()-- Update URL query params without remounting the viewlive_redirect()-- Navigate to a different LiveView over the existing WebSocket- Template directives --
dj-patchanddj-navigatefor declarative navigation - Browser history -- Full back/forward support via
popstatehandling
Quick Start¶
1. Add NavigationMixin to Your View¶
from djust import LiveView
from djust.mixins.navigation import NavigationMixin
from djust.decorators import event_handler
class ProductListView(NavigationMixin, LiveView):
template_name = 'products/list.html'
def mount(self, request, **kwargs):
self.category = "all"
self.page = 1
self.products = []
def handle_params(self, params, uri):
"""Called when URL params change (live_patch or browser back/forward)."""
self.category = params.get("category", "all")
self.page = int(params.get("page", 1))
self.products = self.fetch_products()
@event_handler()
def filter_by_category(self, category="all", **kwargs):
self.live_patch(params={"category": category, "page": 1})
2. Use Navigation Directives¶
<!-- Update URL params without remount -->
<a dj-patch="?category=electronics&page=1">Electronics</a>
<a dj-patch="?category=books&page=1">Books</a>
<!-- Navigate to a different view -->
<a dj-navigate="/products/{{ product.id }}/">View Details</a>
API Reference¶
live_patch(params=None, path=None, replace=False)¶
Update the browser URL without remounting the view. Triggers handle_params() and a re-render. mount() is NOT called again.
# Update query params only
self.live_patch(params={"page": 2})
# Change path and params
self.live_patch(path="/search/", params={"q": "django"})
# Replace current history entry (no back button entry)
self.live_patch(params={"sort": "price"}, replace=True)
live_redirect(path, params=None, replace=False)¶
Navigate to a different LiveView over the existing WebSocket. The current view is unmounted and the new view is mounted fresh.
self.live_redirect("/items/42/")
self.live_redirect("/search/", params={"q": "widgets"})
handle_params(params, uri)¶
Callback invoked when URL params change. Override this to update view state based on the URL.
def handle_params(self, params, uri):
self.category = params.get("category", "all")
self.page = int(params.get("page", 1))
self.results = self.search(self.category, self.page)
Template Directives¶
dj-patch¶
Declarative live_patch. Updates the URL and sends url_change to the server without remounting.
<a dj-patch="?sort=name&order=asc">Sort by Name</a>
<a dj-patch="/products/?category=new">New Products</a>
<a dj-patch="/">Home (root path)</a>
Patching to the root path / is supported and correctly updates the browser URL.
Note: Use dj-patch for navigation instead of dj-click when you need URL updates and browser history support. System check djust.T010 will warn if you use dj-click with navigation-related data attributes like data-view or data-tab.
dj-navigate¶
Declarative live_redirect. Navigates to a different view over the WebSocket.
<a dj-navigate="/dashboard/">Go to Dashboard</a>
<a dj-navigate="/items/{{ item.id }}/">View Item</a>
Example: Search with URL State¶
class SearchView(NavigationMixin, LiveView):
template_name = 'search.html'
def mount(self, request, **kwargs):
self.query = ""
self.sort = "relevance"
self.results = []
def handle_params(self, params, uri):
self.query = params.get("q", "")
self.sort = params.get("sort", "relevance")
if self.query:
self.results = Product.objects.filter(
name__icontains=self.query
).order_by(self.sort)
@event_handler()
def search(self, value="", **kwargs):
self.live_patch(params={"q": value, "sort": self.sort})
@event_handler()
def change_sort(self, sort="relevance", **kwargs):
self.live_patch(params={"q": self.query, "sort": sort})
<input type="text" dj-change="search" value="{{ query }}">
<div class="sort-options">
<a dj-patch="?q={{ query }}&sort=relevance">Relevance</a>
<a dj-patch="?q={{ query }}&sort=price">Price</a>
<a dj-patch="?q={{ query }}&sort=-created">Newest</a>
</div>
{% for product in results %}
<div class="product">
<h3><a dj-navigate="/products/{{ product.id }}/">{{ product.name }}</a></h3>
<p>${{ product.price }}</p>
</div>
{% endfor %}
When to Use Patch vs Redirect¶
Use live_patch() |
Use live_redirect() |
|---|---|
| Filtering, sorting, paginating | Navigating to a different page |
| Changing tabs within the same view | Moving between list and detail views |
| Updating search parameters | Redirecting after form submission |
You want mount() NOT called again |
You need a fresh mount() call |
Best Practices¶
⚠️ Anti-Pattern: Don't Use dj-click for Navigation¶
This is the most common mistake when building multi-view djust apps. Using dj-click to trigger a handler that immediately calls live_redirect() creates an unnecessary round-trip.
❌ Wrong — using dj-click to trigger a handler that calls live_redirect():
# Anti-pattern: Handler does nothing but navigate
@event_handler()
def go_to_item(self, item_id, **kwargs):
self.live_redirect(f"/items/{item_id}/") # Wasteful round-trip!
<!-- Wrong: Forces WebSocket round-trip just to navigate -->
<button dj-click="go_to_item" dj-value-item_id="{{ item.id }}">View</button>
✅ Right — using dj-navigate directly:
<!-- Right: Client navigates immediately, no server round-trip -->
<a dj-navigate="/items/{{ item.id }}/">View Item</a>
Why it matters: Direct navigation is 10-20x faster (~10ms vs 110-250ms), saves WebSocket bandwidth, and provides instant user feedback.
When to Use live_redirect() in Handlers¶
Use handlers for navigation only when navigation depends on server-side logic:
- Conditional navigation after form validation
- Navigation based on auth/permissions checks
- Navigation after async operations (creating records, API calls)
- Multi-step wizard logic with conditional flow
Common theme: The handler does meaningful work before navigating. If your handler only calls live_redirect(), use dj-navigate instead.
Anti-Pattern: Don't Use dj-click for Tab/View Switching¶
Using dj-click with data attributes like data-view or data-tab to switch between sections within a view is fragile and loses URL state. Use dj-patch instead.
The anti-pattern:
<button dj-click="switch_view" data-view="settings">Settings</button>
@event_handler()
def switch_view(self, view="", **kwargs):
self.active_view = view
self._load_data()
Why this breaks:
- Data attributes are fragile -- if the VDOM diff replaces the element mid-click, or the user clicks a child element (e.g. an icon
<span>inside the button), theviewparam can arrive as"", leaving the UI in a broken state. - No URL update -- the browser URL doesn't change, so back/forward doesn't work, tabs aren't bookmarkable, and refreshing always resets to the default view.
- Race conditions -- if
handle_tickfires between the click and the re-render, state can get out of sync because there's no URL as source of truth.
The correct pattern:
<a dj-patch="?tab=settings"
class="{% if active_tab == 'settings' %}active{% endif %}">
Settings
</a>
<a dj-patch="?tab=overview"
class="{% if active_tab == 'overview' %}active{% endif %}">
Overview
</a>
class DashboardView(NavigationMixin, LiveView):
template_name = 'dashboard.html'
VALID_TABS = {"overview", "settings", "logs"}
def mount(self, request, **kwargs):
self.active_tab = "overview"
def handle_params(self, params, uri):
tab = params.get("tab", "overview")
if tab in self.VALID_TABS:
self.active_tab = tab
self._load_tab_data()
Why it works:
dj-patchupdates the URL immediately on the client (no round-trip delay for the URL change)handle_paramsis a first-class lifecycle method with proper re-render sequencing- Browser back/forward and bookmarks work automatically
- Idempotent -- calling
handle_paramstwice with the same params is a no-op - System check
djust.T010detects the anti-pattern and suggestsdj-patch
Rule of thumb:
| Directive | Use for |
|---|---|
dj-click |
Actions that modify state (increment counter, delete item, toggle) |
dj-patch |
Navigation that should update the URL (tabs, filters, pagination) |
dj-navigate |
Full page navigation to a different LiveView |
URL Design Best Practices¶
- Use query params for filter/sort/page state that should be shareable and bookmarkable.
- Use
replace=Truefor transient state changes (e.g., intermediate typing) to avoid polluting browser history. - Always implement
handle_params()to restore state from URL -- this ensures deep links and browser back/forward work correctly. - Keep URL params flat and simple:
?category=books&page=2rather than nested structures.