Component Migration Guide
Table of Contents
- Overview
- Migration Strategy
- Pre-Migration Checklist
- Identifying Component Types
- Migrating Simple Components
- Migrating to LiveComponents
- Updating Parent Views
- Testing After Migration
- Common Migration Scenarios
- Troubleshooting
Overview
This guide helps you migrate existing djust components to the new two-tier architecture (Component vs LiveComponent).
Goals:
- ✅ Maintain backward compatibility where possible
- ✅ Improve performance through proper component types
- ✅ Enable efficient VDOM patching
- ✅ Simplify component code
Timeline: Most migrations can be done incrementally - no need to migrate everything at once!
Migration Strategy
Phase 1: Assessment (1-2 days)
-
Audit existing components
- Count total components
- Categorize by complexity
- Identify dependencies
-
Prioritize migration
- Start with high-value components
- Or start with simple components (quick wins)
Phase 2: Incremental Migration (1-2 weeks)
-
Migrate simple components first
- Badges, buttons, icons
- Quick wins, low risk
-
Migrate complex components
- Tabs, pagination, modals
- Higher value, more testing needed
-
Update parent views
- Add event handlers
- Remove manual coordination
Phase 3: Optimization (ongoing)
- Remove deprecated patterns
- Add prop reactivity
- Performance tuning
Pre-Migration Checklist
Before migrating, ensure you have:
- Read LiveComponent Architecture
- Read API Reference
- Read Best Practices
- Working test suite for existing components
- Git branch for migration work
- List of all components to migrate
Identifying Component Types
Quick Assessment Tool
Run through each component and check:
# For each component, ask:
1. Does it have instance variables that change? (self.foo = ...)
YES → LiveComponent
NO → Continue
2. Does it handle events? (dj-click, dj-change handlers)
YES → LiveComponent
NO → Continue
3. Does it load data or have side effects?
YES → LiveComponent
NO → Continue
4. Is it just rendering props to HTML?
YES → Simple Component (or template syntax!)
NO → Re-evaluate above
5. Is it so simple you could write it in the template?
YES → No component needed!
NO → Simple Component
Examples
# Current component - what type should it be?
class BadgeComponent(LiveComponent): # Current
def mount(self, text, variant):
self.text = text
self.variant = variant
def get_context_data(self):
return {'text': self.text, 'variant': self.variant}
# Assessment:
# - No changing state ✗
# - No event handlers ✗
# - No data loading ✗
# - Pure presentation ✓
#
# VERDICT: Simple Component (or template syntax)
class TabsComponent(LiveComponent): # Current
def mount(self, tabs, active):
self.tabs = tabs
self.active = active
def switch_tab(self, tab):
self.active = tab # Changes state!
self.send_parent("tab_changed", {"tab": tab})
def get_context_data(self):
return {'tabs': self.tabs, 'active': self.active}
# Assessment:
# - Changing state (self.active) ✓
# - Event handler (switch_tab) ✓
# - Sends events to parent ✓
#
# VERDICT: Keep as LiveComponent
Migrating Simple Components
Pattern 1: From LiveComponent to Component
Before (over-engineered):
from djust import LiveComponent
class BadgeComponent(LiveComponent):
template_name = 'components/badge.html'
def mount(self, text, variant="primary"):
self.text = text
self.variant = variant
def get_context_data(self):
return {
'text': self.text,
'variant': self.variant
}
After (simplified):
from djust.components import Component
from django.utils.safestring import mark_safe
class BadgeComponent(Component):
"""Simple stateless badge - no lifecycle needed"""
def __init__(self, text, variant="primary"):
self.text = text
self.variant = variant
def render(self) -> str:
return mark_safe(f'<span class="badge bg-{self.variant}">{self.text}</span>')
Changes:
- Inherit from
Componentinstead ofLiveComponent - Remove
template_name - Change
mount()to__init__() - Change
get_context_data()torender() - Return HTML string directly
Benefits:
- ⬇️ 50% less code
- ⚡ Faster (no VDOM overhead)
- 🔧 Easier to test
- 📝 More Pythonic
Pattern 2: From Component to Template Syntax
Before (component):
class BadgeComponent(Component):
def __init__(self, text, variant="primary"):
self.text = text
self.variant = variant
def render(self):
return f'<span class="badge bg-{self.variant}">{self.text}</span>'
# Usage in view
def get_context_data(self):
return {
'badge': BadgeComponent(text=str(self.count), variant='primary')
}
# Usage in template
{{ badge.render }}
After (template syntax):
# Remove component entirely!
# Usage in view
def get_context_data(self):
return {
'count': self.count,
'badge_variant': 'primary'
}
# Usage in template
<span class="badge bg-{{ badge_variant }}">{{ count }}</span>
When to do this:
- Component used in only 1-2 places
- Very simple HTML
- No framework-specific rendering
Migrating to LiveComponents
Pattern 1: Adding State Management
Before (stateless, recreated every render):
class TabsComponent(Component):
def __init__(self, tabs, active):
self.tabs = tabs
self.active = active
def render(self):
# Returns HTML but loses state on parent re-render
return '...'
# Parent recreates component every time
def get_context_data(self):
return {
'tabs': TabsComponent(tabs=self.tabs, active=self.active_tab)
}
After (stateful, persistent):
class TabsComponent(LiveComponent):
"""Stateful tabs with persistent state"""
template_string = """
<ul class="nav nav-tabs">
{% for tab in tabs %}
<li class="nav-item">
<button dj-click="switch_tab" data-tab="{{ tab.id }}"
class="nav-link {% if tab.id == active %}active{% endif %}">
{{ tab.label }}
</button>
</li>
{% endfor %}
</ul>
"""
def mount(self, tabs, active=None):
"""Called once - initialize state"""
self.tabs = tabs
self.active = active or tabs[0].id
def switch_tab(self, tab):
"""Event handler - updates state"""
self.active = tab
self.send_parent("tab_changed", {"tab": tab})
def get_context_data(self):
return {
'tabs': self.tabs,
'active': self.active
}
# Parent creates component once in mount()
def mount(self, request):
self.tabs_component = TabsComponent(
tabs=[{"id": "home", "label": "Home"}]
)
Key Changes:
- Add
template_string(or keeptemplate_name) - Change
__init__tomount() - Add event handlers (
switch_tab) - Add
send_parent()calls - Create component once in
mount(), not inget_context_data()
Pattern 2: Adding Reactivity
Before (manual prop updates):
class UserDetailComponent(LiveComponent):
def mount(self, user):
self.user = user
self.stats = None
if user:
self._load_stats()
def set_user(self, user):
"""Parent must manually call this"""
self.user = user
self._load_stats()
def _load_stats(self):
self.stats = get_user_stats(self.user)
# Parent manually updates
def handle_user_selected(self, user_id):
user = User.objects.get(id=user_id)
self.user_detail.set_user(user) # Manual call
After (automatic reactivity):
class UserDetailComponent(LiveComponent):
def mount(self, user=None):
self.user = user
self.stats = None
if user:
self._load_stats()
def update(self, **props):
"""Framework calls this when props change"""
if 'user' in props and props['user'] != self.user:
self.user = props['user']
if self.user:
self._load_stats()
else:
self.stats = None
def _load_stats(self):
self.stats = get_user_stats(self.user)
# Parent just updates state - component reacts automatically!
def handle_user_selected(self, user_id):
self.selected_user = User.objects.get(id=user_id)
# Framework calls user_detail.update(user=self.selected_user)
Key Changes:
- Add
update(**props)method - Check for prop changes
- React to changes (load data, update state)
- Remove manual
set_*methods - Parent just updates state
Updating Parent Views
Pattern 1: Moving from Manual to Event-Based Coordination
Before (manual coordination):
class DashboardView(LiveView):
def mount(self, request):
self.selected_user = None
self.user_list = UserListComponent(users=User.objects.all())
self.user_detail = UserDetailComponent()
def select_user(self, user_id):
"""Called directly by template button"""
self.selected_user = User.objects.get(id=user_id)
# Manual coordination
self.user_list.set_active(user_id)
self.user_detail.set_user(self.selected_user)
self.activity_log.add_entry(f"Viewed user {user_id}")
After (event-based):
class DashboardView(LiveView):
def mount(self, request):
self.selected_user_id = None
self.user_list = UserListComponent(users=User.objects.all())
self.user_detail = UserDetailComponent()
self.activity_log = ActivityLogComponent()
def handle_component_event(self, component_id, event, data):
"""Single coordination point"""
if event == "user_selected":
self.selected_user_id = data["user_id"]
# Components automatically update via props!
# UserListComponent sends event instead of calling parent
class UserListComponent(LiveComponent):
def select_user(self, user_id):
self.selected_id = user_id
self.send_parent("user_selected", {"user_id": user_id})
Pattern 2: Using Computed Properties
Before (manual state synchronization):
class DashboardView(LiveView):
def mount(self, request):
self.selected_user_id = None
self.selected_user = None
self.user_permissions = []
def handle_user_selected(self, user_id):
# Manual sync of related state
self.selected_user_id = user_id
self.selected_user = User.objects.get(id=user_id)
self.user_permissions = self.selected_user.get_permissions()
After (computed properties):
class DashboardView(LiveView):
def mount(self, request):
self.selected_user_id = None
@property
def selected_user(self):
"""Automatically computed when selected_user_id changes"""
if self.selected_user_id:
return User.objects.get(id=self.selected_user_id)
return None
@property
def user_permissions(self):
"""Automatically computed from selected_user"""
if self.selected_user:
return self.selected_user.get_permissions()
return []
def handle_component_event(self, component_id, event, data):
if event == "user_selected":
self.selected_user_id = data["user_id"]
# selected_user and user_permissions automatically update!
Testing After Migration
Test Simple Components
def test_badge_component():
"""Simple components are pure functions - easy to test"""
badge = BadgeComponent(text="New", variant="primary")
html = badge.render()
assert 'badge' in html
assert 'bg-primary' in html
assert 'New' in html
# No mocking, no setup, just pure function testing!
Test LiveComponents
def test_tabs_component_state():
"""Test component manages state correctly"""
tabs = [{"id": "home", "label": "Home"}, {"id": "about", "label": "About"}]
component = TabsComponent(tabs=tabs, active="home")
# Test initial state
assert component.active == "home"
# Test state change
component.switch_tab(tab="about")
assert component.active == "about"
def test_tabs_component_events():
"""Test component sends events to parent"""
tabs = [{"id": "home", "label": "Home"}]
component = TabsComponent(tabs=tabs)
# Mock parent communication
events = []
component.send_parent = lambda event, data: events.append((event, data))
# Trigger event
component.switch_tab(tab="home")
# Verify event sent
assert events == [("tab_changed", {"tab": "home"})]
Test Parent Coordination
def test_parent_coordinates_components():
"""Test parent handles component events"""
view = DashboardView()
request = RequestFactory().get('/')
view.mount(request)
# Simulate component event
view.handle_component_event(
component_id="user_list",
event="user_selected",
data={"user_id": 123}
)
# Verify parent state updated
assert view.selected_user_id == 123
# Verify computed properties work
assert view.selected_user is not None
Common Migration Scenarios
Scenario 1: Badge Component
Current:
class BadgeComponent(LiveComponent):
template_name = 'components/badge.html'
def mount(self, text, variant="primary"):
self.text = text
self.variant = variant
Migration Decision: Simple Component (or template syntax)
Migrated:
# Option A: Simple Component
class BadgeComponent(Component):
def __init__(self, text, variant="primary"):
self.text = text
self.variant = variant
def render(self):
return f'<span class="badge bg-{self.variant}">{self.text}</span>'
# Option B: Template Syntax (recommended)
# Just use: <span class="badge bg-{{ variant }}">{{ text }}</span>
Scenario 2: Tabs Component
Current:
class TabsComponent(LiveComponent):
template_name = 'components/tabs.html'
def mount(self, tabs, active):
self.tabs = tabs
self.active = active
def activate_tab(self, tab_id):
self.active = tab_id
self.trigger_update() # Old API
Migration Decision: Stay as LiveComponent (has state)
Migrated:
class TabsComponent(LiveComponent):
template_string = """
<ul class="nav nav-tabs">
{% for tab in tabs %}
<button dj-click="switch_tab" data-tab="{{ tab.id }}"
class="nav-link {% if tab.id == active %}active{% endif %}">
{{ tab.label }}
</button>
{% endfor %}
</ul>
"""
def mount(self, tabs, active=None):
self.tabs = tabs
self.active = active or tabs[0].id
def switch_tab(self, tab):
self.active = tab
self.send_parent("tab_changed", {"tab": tab}) # New API
def get_context_data(self):
return {'tabs': self.tabs, 'active': self.active}
Changes:
- Removed
activate_tab()(called by parent) → Changed toswitch_tab()(called by user) - Added
send_parent()to notify parent - Added
template_string(optional - could keep template file)
Scenario 3: Form Component
Current:
class UserFormComponent(LiveComponent):
def mount(self, user=None):
self.user = user
self.errors = {}
def validate_field(self, field, value):
# Validation logic
if not value:
self.errors[field] = "Required"
else:
self.errors.pop(field, None)
def submit_form(self, form_data):
# Validate all fields
if self.validate_all(form_data):
# Save to database
if self.user:
self.user.update(**form_data)
else:
self.user = User.objects.create(**form_data)
# Notify parent
self.trigger_update() # Old API
Migration Decision: Stay as LiveComponent, improve event handling
Migrated:
class UserFormComponent(LiveComponent):
def mount(self, user=None):
self.user = user
self.errors = {}
self.is_submitting = False
def validate_field(self, field, value):
"""Real-time field validation"""
if not value:
self.errors[field] = "Required"
else:
self.errors.pop(field, None)
def submit_form(self, **form_data):
"""Submit form and notify parent"""
self.is_submitting = True
if self.validate_all(form_data):
if self.user:
self.user.update(**form_data)
else:
self.user = User.objects.create(**form_data)
# New API: Send success event to parent
self.send_parent("form_submitted", {
"user_id": self.user.id,
"action": "update" if self.user else "create"
})
else:
# Send error event
self.send_parent("form_error", {
"errors": self.errors
})
self.is_submitting = False
def get_context_data(self):
return {
'user': self.user,
'errors': self.errors,
'is_submitting': self.is_submitting
}
Changes:
- Added
send_parent()for success/error events - Added
is_submittingstate for UX - Removed
trigger_update()(automatic)
Troubleshooting
Issue 1: Component State Lost on Re-render
Symptom: Component resets every time parent updates
Cause: Creating component in get_context_data() instead of mount()
Fix:
# ❌ Wrong - recreates every render
def get_context_data(self):
return {
'tabs': TabsComponent(tabs=self.tabs) # New instance!
}
# ✅ Correct - create once in mount
def mount(self, request):
self.tabs = TabsComponent(tabs=self.tab_data)
def get_context_data(self):
return {
'tabs': self.tabs # Same instance
}
Issue 2: Events Not Working
Symptom: Clicking buttons does nothing
Cause: Not sending events to parent, or parent not handling them
Fix:
# In component - send event
def switch_tab(self, tab):
self.active = tab
self.send_parent("tab_changed", {"tab": tab}) # Add this!
# In parent - handle event
def handle_component_event(self, component_id, event, data):
if event == "tab_changed":
self.current_tab = data["tab"] # Add handler!
Issue 3: Props Not Updating
Symptom: Component doesn't react to parent state changes
Cause: Missing update() method
Fix:
class UserDetailComponent(LiveComponent):
def mount(self, user=None):
self.user = user
# Add this method!
def update(self, **props):
if 'user' in props:
self.user = props['user']
self._load_user_data()
Issue 4: VDOM Patches Not Working
Symptom: Full page refresh instead of patches
Cause: Template structure changed (dynamic template_string)
Fix: Use static template_string, not @property
# ❌ Wrong - dynamic template breaks VDOM
@property
def template_string(self):
return f"""<div>{self.build_html()}</div>"""
# ✅ Correct - static template
template_string = """
<div>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</div>
"""
Migration Checklist
Use this checklist for each component:
- Identified component type (simple or stateful)
- Migrated class definition
-
Migrated
mount()/__init__() - Migrated template (file or string)
- Added event handlers if needed
-
Added
send_parent()calls -
Added
update()method if reactive -
Updated parent to create component in
mount() - Updated parent to handle events
- Added tests
- Verified VDOM patching works
- Performance tested
- Documentation updated
Summary
Migration Path:
- Audit → Identify component types
- Simplify → Convert to simple components or template syntax
- Enhance → Add state and events to LiveComponents
- Coordinate → Update parent event handling
- Test → Verify behavior and performance
- Optimize → Add reactivity, computed properties
Remember:
- Migrate incrementally (not all at once)
- Test thoroughly after each migration
- Simple components first (quick wins)
- Parent coordination is key
Next Steps:
- Examples - See complete migrated examples
- Best Practices - Learn patterns
- API Reference - Detailed API