Skip to content

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).

A/B Testing

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)

{% component "offer_card" use_new_version=False savings_percent=10 / %}

Variant B (New)

{% component "offer_card" use_new_version=True savings_percent=25 / %}

Variant C (Random)

{% component "offer_card" savings_percent=15 / %}

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)

urls.pyยค

from django.urls import path

from examples.pages.ab_testing import ABTestingPage

urlpatterns = [
    path("examples/ab_testing", ABTestingPage.as_view(), name="ab_testing"),
]