A/B Testingยค
A/B testing, phased rollouts, or other advanced use cases can be made easy by dynamically rendering different versions of a component.
Use the Component.on_render()
hook, to decide which version to render based on a component parameter (or a random choice).
How it worksยค
Component.on_render()
is called when the component is being rendered. This method can completely override the rendering process, so we can use it to render another component in its place.
class OfferCard(Component):
...
def on_render(self, context, template):
# Pass all kwargs to the child component
kwargs_for_child = self.kwargs._asdict()
use_new = kwargs_for_child.pop("use_new_version")
# If version not specified, choose randomly
if use_new is None:
use_new = random.choice([True, False])
if use_new:
return OfferCardNew.render(context=context, kwargs=kwargs_for_child)
else:
return OfferCardOld.render(context=context, kwargs=kwargs_for_child)
In the example we render 3 versions of the OfferCard
component:
- Variant that always shows an "old" version with
use_new_version=False
- Variant that always shows a "new" version with
use_new_version=True
. - Variant that randomly shows one or the other, omitting the
use_new_version
flag.
All extra parameters are passed through to the underlying components.
Variant A (Old)
Variant B (New)
Variant C (Random)
Definitionยค
# ruff: noqa: S311
import random
from typing import NamedTuple, Optional
from django_components import Component, register, types
DESCRIPTION = "Dynamically render different component versions. Use for A/B testing, phased rollouts, etc."
@register("offer_card_old")
class OfferCardOld(Component):
class Kwargs(NamedTuple):
savings_percent: int
def get_template_data(self, args, kwargs, slots, context):
return {
"savings_percent": kwargs.savings_percent,
}
template: types.django_html = """
<div class="p-4 border rounded-lg bg-gray-100">
<h3 class="text-lg font-bold text-gray-800">
Special Offer!
</h3>
<p class="text-gray-600">
Get {{ savings_percent }}% off on your next purchase.
</p>
</div>
"""
@register("offer_card_new")
class OfferCardNew(OfferCardOld):
template: types.django_html = """
<div class="p-6 border-2 border-dashed border-blue-500 rounded-lg bg-blue-50 text-center">
<h3 class="text-xl font-extrabold text-blue-800 animate-pulse">
FLASH SALE!
</h3>
<p class="text-blue-600">
Exclusive Offer: {{ savings_percent }}% off everything!
</p>
</div>
"""
@register("offer_card")
class OfferCard(Component):
class Kwargs(NamedTuple):
savings_percent: int
use_new_version: Optional[bool] = None
def on_render(self, context, template):
# Pass all kwargs to the child component
kwargs_for_child = self.kwargs._asdict()
use_new = kwargs_for_child.pop("use_new_version")
# If version not specified, choose randomly
if use_new is None:
use_new = random.choice([True, False])
if use_new:
return OfferCardNew.render(context=context, kwargs=kwargs_for_child)
else:
return OfferCardOld.render(context=context, kwargs=kwargs_for_child)
Exampleยค
To see the component in action, you can set up a view and a URL pattern as shown below.
views.py
ยค
from django.http import HttpRequest, HttpResponse
from django_components import Component, types
class ABTestingPage(Component):
class Media:
js = ("https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,container-queries",)
template: types.django_html = """
{% load component_tags %}
<html>
<head>
<title>A/B Testing Example</title>
</head>
<body class="bg-gray-100 p-8">
<div class="max-w-2xl mx-auto bg-white p-6 rounded-lg shadow-md">
<h1 class="text-2xl font-bold mb-4">
A/B Testing Components
</h1>
<p class="text-gray-600 mb-6">
This example shows how a single component can render different versions
based on a parameter (or a random choice), perfect for A/B testing.
</p>
<div class="mb-8">
<h2 class="text-xl font-semibold mb-2">
Variant A (Old Offer)
</h2>
<p class="text-sm text-gray-500 mb-2">
Rendered with <code>use_new_version=False</code>
</p>
{% component "offer_card" use_new_version=False savings_percent=10 / %}
</div>
<div>
<h2 class="text-xl font-semibold mb-2">
Variant B (New Offer)
</h2>
<p class="text-sm text-gray-500 mb-2">
Rendered with <code>use_new_version=True</code>
</p>
{% component "offer_card" use_new_version=True savings_percent=25 / %}
</div>
<div class="mt-8">
<h2 class="text-xl font-semibold mb-2">
Variant C (Random)
</h2>
<p class="text-sm text-gray-500 mb-2">
Rendered without <code>use_new_version</code>.
Reload the page to see a different version.
</p>
{% component "offer_card" savings_percent=15 / %}
</div>
</div>
</body>
</html>
"""
class View:
def get(self, request: HttpRequest) -> HttpResponse:
return ABTestingPage.render_to_response(request=request)