diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4546daec..92d870d2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,7 +36,7 @@ Using the following categories, list your changes in this order:
 
 -   Nothing (yet)
 
-## [3.0.0a2] - 2023-02-02
+## [3.0.0a3] - 2023-02-21
 
 ???+ note
 
@@ -45,20 +45,21 @@ Using the following categories, list your changes in this order:
     To upgrade from previous version you will need to...
 
     1. Install `django-idom >= 3.0.0`
-    2. Run `idom update-html-usages <DIR>` to update your `idom.html.*` calls to the new syntax
+    2. Run `idom rewrite-keys <DIR>` and `idom rewrite-camel-case-props <DIR>` to update your `idom.html.*` calls to the new syntax
     3. Run `python manage.py migrate` to create the new Django-IDOM database entries
 
 ### Added
 
 -   The `idom` client will automatically configure itself to debug mode depending on `settings.py:DEBUG`.
--   `use_connection` hook for returning the browser's active `Connection`
+-   `use_connection` hook for returning the browser's active `Connection`.
+-   `IDOM_CACHE` is now configurable within `settings.py` to whatever cache name you wish.
 
 ### Changed
 
 -   It is now mandatory to run `manage.py migrate` after installing IDOM.
--   Bumped the minimum IDOM version to 1.0.0
-    -   Due to IDOM 1.0.0, `idom.html.*`, HTML properties are now `snake_case` `**kwargs` rather than a `dict` of values.
-    -   You can auto-convert to the new style using `idom update-html-usages <DIR>`.
+-   Bumped the minimum IDOM version to 1.0.0. Due to IDOM 1.0.0, `idom.html.*`...
+    -   HTML properties can now be `snake_case`. For example `className` now becomes `class_name`.
+    -   `key=...` is now declared within the props `dict` (rather than as a `kwarg`).
 -   The `component` template tag now supports both positional and keyword arguments.
 -   The `component` template tag now supports non-serializable arguments.
 -   `IDOM_WS_MAX_RECONNECT_TIMEOUT` setting has been renamed to `IDOM_RECONNECT_MAX`.
@@ -66,13 +67,14 @@ Using the following categories, list your changes in this order:
 ### Removed
 
 -   `django_idom.hooks.use_websocket` has been removed. The similar replacement is `django_idom.hooks.use_connection`.
--   `django_idom.types.IdomWebsocket` has been removed. The similar replacement is `django_idom.types.Connection`
+-   `django_idom.types.IdomWebsocket` has been removed. The similar replacement is `django_idom.types.Connection`.
+-   `settings.py:CACHE['idom']` is no longer used by default. The name of the cache back-end must now be specified with the `IDOM_CACHE` setting.
 
 ### Fixed
 
--   `view_to_component` will now retain any HTML that was defined in a `<head>` tag.
+-   `view_to_component` will now retain the contents of a `<head>` tag when rendering.
 -   React client is now set to `production` rather than `development`.
--   `use_query` will now utilize `field.related_name` when postprocessing many-to-one relationships
+-   `use_query` will now utilize `field.related_name` when postprocessing many-to-one relationships.
 
 ### Security
 
@@ -246,8 +248,8 @@ Using the following categories, list your changes in this order:
 
 -   Support for IDOM within the Django
 
-[unreleased]: https://github.com/idom-team/django-idom/compare/3.0.0a2...HEAD
-[3.0.0a2]: https://github.com/idom-team/django-idom/compare/2.2.1...3.0.0a2
+[unreleased]: https://github.com/idom-team/django-idom/compare/3.0.0a3...HEAD
+[3.0.0a3]: https://github.com/idom-team/django-idom/compare/2.2.1...3.0.0a3
 [2.2.1]: https://github.com/idom-team/django-idom/compare/2.2.0...2.2.1
 [2.2.0]: https://github.com/idom-team/django-idom/compare/2.1.0...2.2.0
 [2.1.0]: https://github.com/idom-team/django-idom/compare/2.0.1...2.1.0
diff --git a/docs/python/settings.py b/docs/python/settings.py
index f7ee0f15..c8559b4a 100644
--- a/docs/python/settings.py
+++ b/docs/python/settings.py
@@ -1,8 +1,8 @@
-# If "idom" cache is not configured, then "default" will be used
-# IDOM works best with a multiprocessing-safe and thread-safe cache backend.
-CACHES = {
-    "idom": {"BACKEND": ...},
-}
+# IDOM requires a multiprocessing-safe and thread-safe cache.
+IDOM_CACHE = "default"
+
+# IDOM requires a multiprocessing-safe and thread-safe database.
+IDOM_DATABASE = "default"
 
 # Maximum seconds between reconnection attempts before giving up.
 # Use `0` to prevent component reconnection.
@@ -11,5 +11,5 @@
 # The URL for IDOM to serve the component rendering websocket
 IDOM_WEBSOCKET_URL = "idom/"
 
-# Dotted path to the default postprocessor function, or `None`
+# Dotted path to the default `django_idom.hooks.use_query` postprocessor function, or `None`
 IDOM_DEFAULT_QUERY_POSTPROCESSOR = "example_project.utils.my_postprocessor"
diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt
index dc87a347..c17d5d71 100644
--- a/requirements/pkg-deps.txt
+++ b/requirements/pkg-deps.txt
@@ -1,5 +1,5 @@
 channels >=4.0.0
-idom >=1.0.0a3, <1.1.0
+idom >=1.0.0a5, <1.1.0
 aiofile >=3.0
 dill >=0.3.5
 typing_extensions
diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py
index 2856d386..7cfa23a9 100644
--- a/src/django_idom/__init__.py
+++ b/src/django_idom/__init__.py
@@ -2,7 +2,7 @@
 from django_idom.websocket.paths import IDOM_WEBSOCKET_PATH
 
 
-__version__ = "3.0.0a2"
+__version__ = "3.0.0a3"
 __all__ = [
     "IDOM_WEBSOCKET_PATH",
     "hooks",
diff --git a/src/django_idom/apps.py b/src/django_idom/apps.py
index f91b50b7..bde9fb98 100644
--- a/src/django_idom/apps.py
+++ b/src/django_idom/apps.py
@@ -1,7 +1,7 @@
 import logging
 
 from django.apps import AppConfig
-from django.db.utils import OperationalError
+from django.db.utils import DatabaseError
 
 from django_idom.utils import ComponentPreloader, db_cleanup
 
@@ -22,5 +22,7 @@ def ready(self):
         # where the database may not be ready.
         try:
             db_cleanup(immediate=True)
-        except OperationalError:
-            _logger.debug("IDOM database was not ready at startup. Skipping cleanup...")
+        except DatabaseError:
+            _logger.debug(
+                "Could not access IDOM database at startup. Skipping cleanup..."
+            )
diff --git a/src/django_idom/components.py b/src/django_idom/components.py
index 4448371f..c57f87ae 100644
--- a/src/django_idom/components.py
+++ b/src/django_idom/components.py
@@ -5,6 +5,7 @@
 from typing import Any, Callable, Protocol, Sequence, Union, cast, overload
 
 from django.contrib.staticfiles.finders import find
+from django.core.cache import caches
 from django.http import HttpRequest
 from django.urls import reverse
 from django.views import View
@@ -76,7 +77,10 @@ async def async_render():
             view, _args, _kwargs
         )
         return html.iframe(
-            src=reverse("idom:view_to_component", args=[dotted_path]), loading="lazy"
+            {
+                "src": reverse("idom:view_to_component", args=[dotted_path]),
+                "loading": "lazy",
+            }
         )
 
     # Return the view if it's been rendered via the `async_render` hook
@@ -209,12 +213,12 @@ def _cached_static_contents(static_path: str):
     # Cache is preferrable to `use_memo` due to multiprocessing capabilities
     last_modified_time = os.stat(abs_path).st_mtime
     cache_key = f"django_idom:static_contents:{static_path}"
-    file_contents = IDOM_CACHE.get(cache_key, version=int(last_modified_time))
+    file_contents = caches[IDOM_CACHE].get(cache_key, version=int(last_modified_time))
     if file_contents is None:
         with open(abs_path, encoding="utf-8") as static_file:
             file_contents = static_file.read()
-        IDOM_CACHE.delete(cache_key)
-        IDOM_CACHE.set(
+        caches[IDOM_CACHE].delete(cache_key)
+        caches[IDOM_CACHE].set(
             cache_key, file_contents, timeout=None, version=int(last_modified_time)
         )
 
diff --git a/src/django_idom/config.py b/src/django_idom/config.py
index a7b4ca8d..b276b006 100644
--- a/src/django_idom/config.py
+++ b/src/django_idom/config.py
@@ -3,7 +3,8 @@
 from typing import Dict
 
 from django.conf import settings
-from django.core.cache import DEFAULT_CACHE_ALIAS, BaseCache, caches
+from django.core.cache import DEFAULT_CACHE_ALIAS
+from django.db import DEFAULT_DB_ALIAS
 from idom.config import IDOM_DEBUG_MODE
 from idom.core.types import ComponentConstructor
 
@@ -24,10 +25,15 @@
     "IDOM_RECONNECT_MAX",
     259200,  # Default to 3 days
 )
-IDOM_CACHE: BaseCache = (
-    caches["idom"]
-    if "idom" in getattr(settings, "CACHES", {})
-    else caches[DEFAULT_CACHE_ALIAS]
+IDOM_CACHE: str = getattr(
+    settings,
+    "IDOM_CACHE",
+    DEFAULT_CACHE_ALIAS,
+)
+IDOM_DATABASE: str = getattr(
+    settings,
+    "IDOM_DATABASE",
+    DEFAULT_DB_ALIAS,
 )
 IDOM_DEFAULT_QUERY_POSTPROCESSOR: Postprocessor | None = import_dotted_path(
     getattr(
diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py
index dd17ca16..ad40d301 100644
--- a/src/django_idom/hooks.py
+++ b/src/django_idom/hooks.py
@@ -7,7 +7,6 @@
     Awaitable,
     Callable,
     DefaultDict,
-    MutableMapping,
     Sequence,
     Union,
     cast,
@@ -65,9 +64,14 @@ def use_origin() -> str | None:
         return None
 
 
-def use_scope() -> MutableMapping[str, Any]:
+def use_scope() -> dict[str, Any]:
     """Get the current ASGI scope dictionary"""
-    return _use_scope()
+    scope = _use_scope()
+
+    if isinstance(scope, dict):
+        return scope
+
+    raise TypeError(f"Expected scope to be a dict, got {type(scope)}")
 
 
 def use_connection() -> Connection:
diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py
index 5fffde7e..384c4740 100644
--- a/src/django_idom/http/views.py
+++ b/src/django_idom/http/views.py
@@ -1,6 +1,7 @@
 import os
 
 from aiofile import async_open
+from django.core.cache import caches
 from django.core.exceptions import SuspiciousOperation
 from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
 from idom.config import IDOM_WEB_MODULES_DIR
@@ -25,12 +26,12 @@ async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse:
     # Fetch the file from cache, if available
     last_modified_time = os.stat(path).st_mtime
     cache_key = create_cache_key("web_module", str(path).lstrip(str(web_modules_dir)))
-    response = await IDOM_CACHE.aget(cache_key, version=int(last_modified_time))
+    response = await caches[IDOM_CACHE].aget(cache_key, version=int(last_modified_time))
     if response is None:
         async with async_open(path, "r") as fp:
             response = HttpResponse(await fp.read(), content_type="text/javascript")
-        await IDOM_CACHE.adelete(cache_key)
-        await IDOM_CACHE.aset(
+        await caches[IDOM_CACHE].adelete(cache_key)
+        await caches[IDOM_CACHE].aset(
             cache_key, response, timeout=None, version=int(last_modified_time)
         )
     return response
diff --git a/src/django_idom/migrations/0003_componentsession_delete_componentparams.py b/src/django_idom/migrations/0003_componentsession_delete_componentparams.py
new file mode 100644
index 00000000..7a5c98e6
--- /dev/null
+++ b/src/django_idom/migrations/0003_componentsession_delete_componentparams.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.1.6 on 2023-02-21 10:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("django_idom", "0002_rename_created_at_componentparams_last_accessed"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="ComponentSession",
+            fields=[
+                (
+                    "uuid",
+                    models.UUIDField(
+                        editable=False, primary_key=True, serialize=False, unique=True
+                    ),
+                ),
+                ("params", models.BinaryField()),
+                ("last_accessed", models.DateTimeField(auto_now_add=True)),
+            ],
+        ),
+        migrations.DeleteModel(
+            name="ComponentParams",
+        ),
+    ]
diff --git a/src/django_idom/models.py b/src/django_idom/models.py
index 1e67f368..219866f2 100644
--- a/src/django_idom/models.py
+++ b/src/django_idom/models.py
@@ -1,7 +1,11 @@
 from django.db import models
 
 
-class ComponentParams(models.Model):
+class ComponentSession(models.Model):
+    """A model for storing component parameters.
+    All queries must be routed through `django_idom.config.IDOM_DATABASE`.
+    """
+
     uuid = models.UUIDField(primary_key=True, editable=False, unique=True)  # type: ignore
-    data = models.BinaryField(editable=False)  # type: ignore
+    params = models.BinaryField(editable=False)  # type: ignore
     last_accessed = models.DateTimeField(auto_now_add=True)  # type: ignore
diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py
index 4309177b..da13b352 100644
--- a/src/django_idom/templatetags/idom.py
+++ b/src/django_idom/templatetags/idom.py
@@ -45,7 +45,7 @@ def component(dotted_path: str, *args, **kwargs):
     try:
         if func_has_params(component, *args, **kwargs):
             params = ComponentParamData(args, kwargs)
-            model = models.ComponentParams(uuid=uuid, data=pickle.dumps(params))
+            model = models.ComponentSession(uuid=uuid, params=pickle.dumps(params))
             model.full_clean()
             model.save()
     except TypeError as e:
diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py
index a0963b35..c360bddd 100644
--- a/src/django_idom/utils.py
+++ b/src/django_idom/utils.py
@@ -12,6 +12,7 @@
 from typing import Any, Callable, Sequence
 
 from channels.db import database_sync_to_async
+from django.core.cache import caches
 from django.db.models import ManyToManyField, prefetch_related_objects
 from django.db.models.base import Model
 from django.db.models.fields.reverse_related import ManyToOneRel
@@ -311,12 +312,12 @@ def create_cache_key(*args):
 def db_cleanup(immediate: bool = False):
     """Deletes expired component parameters from the database.
     This function may be expanded in the future to include additional cleanup tasks."""
-    from .config import IDOM_CACHE, IDOM_RECONNECT_MAX
-    from .models import ComponentParams
+    from .config import IDOM_CACHE, IDOM_DATABASE, IDOM_RECONNECT_MAX
+    from .models import ComponentSession
 
     cache_key: str = create_cache_key("last_cleaned")
     now_str: str = datetime.strftime(timezone.now(), DATE_FORMAT)
-    cleaned_at_str: str = IDOM_CACHE.get(cache_key)
+    cleaned_at_str: str = caches[IDOM_CACHE].get(cache_key)
     cleaned_at: datetime = timezone.make_aware(
         datetime.strptime(cleaned_at_str or now_str, DATE_FORMAT)
     )
@@ -324,7 +325,7 @@ def db_cleanup(immediate: bool = False):
     expires_by: datetime = timezone.now() - timedelta(seconds=IDOM_RECONNECT_MAX)
 
     # Component params exist in the DB, but we don't know when they were last cleaned
-    if not cleaned_at_str and ComponentParams.objects.all():
+    if not cleaned_at_str and ComponentSession.objects.using(IDOM_DATABASE).all():
         _logger.warning(
             "IDOM has detected component sessions in the database, "
             "but no timestamp was found in cache. This may indicate that "
@@ -334,5 +335,7 @@ def db_cleanup(immediate: bool = False):
     # Delete expired component parameters
     # Use timestamps in cache (`cleaned_at_str`) as a no-dependency rate limiter
     if immediate or not cleaned_at_str or timezone.now() >= clean_needed_by:
-        ComponentParams.objects.filter(last_accessed__lte=expires_by).delete()
-        IDOM_CACHE.set(cache_key, now_str)
+        ComponentSession.objects.using(IDOM_DATABASE).filter(
+            last_accessed__lte=expires_by
+        ).delete()
+        caches[IDOM_CACHE].set(cache_key, now_str)
diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py
index 99f860ba..e0cf56c4 100644
--- a/src/django_idom/websocket/consumer.py
+++ b/src/django_idom/websocket/consumer.py
@@ -55,7 +55,11 @@ async def receive_json(self, content: Any, **_) -> None:
 
     async def _run_dispatch_loop(self):
         from django_idom import models
-        from django_idom.config import IDOM_RECONNECT_MAX, IDOM_REGISTERED_COMPONENTS
+        from django_idom.config import (
+            IDOM_DATABASE,
+            IDOM_RECONNECT_MAX,
+            IDOM_REGISTERED_COMPONENTS,
+        )
 
         scope = self.scope
         dotted_path = scope["url_route"]["kwargs"]["dotted_path"]
@@ -91,20 +95,22 @@ async def _run_dispatch_loop(self):
                     await convert_to_async(db_cleanup)()
 
                     # Get the queries from a DB
-                    params_query = await models.ComponentParams.objects.aget(
+                    params_query = await models.ComponentSession.objects.using(
+                        IDOM_DATABASE
+                    ).aget(
                         uuid=uuid,
                         last_accessed__gt=now - timedelta(seconds=IDOM_RECONNECT_MAX),
                     )
                     params_query.last_accessed = timezone.now()
                     await convert_to_async(params_query.save)()
-                except models.ComponentParams.DoesNotExist:
+                except models.ComponentSession.DoesNotExist:
                     _logger.warning(
                         f"Browser has attempted to access '{dotted_path}', "
                         f"but the component has already expired beyond IDOM_RECONNECT_MAX. "
                         "If this was expected, this warning can be ignored."
                     )
                     return
-                component_params: ComponentParamData = pickle.loads(params_query.data)
+                component_params: ComponentParamData = pickle.loads(params_query.params)
                 component_args = component_params.args
                 component_kwargs = component_params.kwargs
 
diff --git a/src/js/package-lock.json b/src/js/package-lock.json
index 83e0ec39..d58618b9 100644
--- a/src/js/package-lock.json
+++ b/src/js/package-lock.json
@@ -5,7 +5,7 @@
 	"packages": {
 		"": {
 			"dependencies": {
-				"idom-client-react": "^1.0.0-a2"
+				"idom-client-react": "^1.0.0-a5"
 			},
 			"devDependencies": {
 				"@rollup/plugin-commonjs": "^24.0.1",
@@ -242,9 +242,9 @@
 			"integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q=="
 		},
 		"node_modules/idom-client-react": {
-			"version": "1.0.0-a2",
-			"resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-1.0.0-a2.tgz",
-			"integrity": "sha512-mfpyPXfM8R4lvgd45DJg+tn/tc5gKNxM32sQPaUr5oWFmt81f1nhWHLmM6RlNv/hB1n51023QCcU4Fj0NCmleg==",
+			"version": "1.0.0-a5",
+			"resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-1.0.0-a5.tgz",
+			"integrity": "sha512-uzkg0qqtEY7xGcc3Yyc3IUzve6aGs0zvxQjf1xFrCxl8XMitdjPYQHdGpOxj+QHvmxgjETkH+5mcR/BO3OlZCQ==",
 			"dependencies": {
 				"htm": "^3.0.3",
 				"json-pointer": "^0.6.2"
@@ -661,9 +661,9 @@
 			"integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q=="
 		},
 		"idom-client-react": {
-			"version": "1.0.0-a2",
-			"resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-1.0.0-a2.tgz",
-			"integrity": "sha512-mfpyPXfM8R4lvgd45DJg+tn/tc5gKNxM32sQPaUr5oWFmt81f1nhWHLmM6RlNv/hB1n51023QCcU4Fj0NCmleg==",
+			"version": "1.0.0-a5",
+			"resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-1.0.0-a5.tgz",
+			"integrity": "sha512-uzkg0qqtEY7xGcc3Yyc3IUzve6aGs0zvxQjf1xFrCxl8XMitdjPYQHdGpOxj+QHvmxgjETkH+5mcR/BO3OlZCQ==",
 			"requires": {
 				"htm": "^3.0.3",
 				"json-pointer": "^0.6.2"
diff --git a/src/js/package.json b/src/js/package.json
index 06e00606..03ad2b57 100644
--- a/src/js/package.json
+++ b/src/js/package.json
@@ -17,6 +17,6 @@
 		"@rollup/plugin-replace": "^5.0.2"
 	},
 	"dependencies": {
-		"idom-client-react": "^1.0.0-a3"
+		"idom-client-react": "^1.0.0-a5"
 	}
 }
diff --git a/tests/test_app/admin.py b/tests/test_app/admin.py
index e0701a96..0ae73f9f 100644
--- a/tests/test_app/admin.py
+++ b/tests/test_app/admin.py
@@ -1,7 +1,7 @@
 from django.contrib import admin
 from test_app.models import ForiegnChild, RelationalChild, RelationalParent, TodoItem
 
-from django_idom.models import ComponentParams
+from django_idom.models import ComponentSession
 
 
 @admin.register(TodoItem)
@@ -24,6 +24,6 @@ class ForiegnChildAdmin(admin.ModelAdmin):
     pass
 
 
-@admin.register(ComponentParams)
+@admin.register(ComponentSession)
 class ComponentParamsAdmin(admin.ModelAdmin):
     list_display = ("uuid", "last_accessed")
diff --git a/tests/test_app/components.py b/tests/test_app/components.py
index 042b9aa7..f9ea53ce 100644
--- a/tests/test_app/components.py
+++ b/tests/test_app/components.py
@@ -15,7 +15,7 @@
 
 @component
 def hello_world():
-    return html._(html.div("Hello World!", id="hello-world"), html.hr())
+    return html._(html.div({"id": "hello-world"}, "Hello World!"), html.hr())
 
 
 @component
@@ -25,11 +25,12 @@ def button():
         html.div(
             "button:",
             html.button(
+                {"id": "counter-inc", "on_click": lambda event: set_count(count + 1)},
                 "Click me!",
-                id="counter-inc",
-                on_click=lambda event: set_count(count + 1),
             ),
-            html.p(f"Current count is: {count}", id="counter-num", data_count=count),
+            html.p(
+                {"id": "counter-num", "data-count": count}, f"Current count is: {count}"
+            ),
         ),
         html.hr(),
     )
@@ -40,9 +41,8 @@ def parameterized_component(x, y):
     total = x + y
     return html._(
         html.div(
+            {"id": "parametrized-component", "data-value": total},
             f"parameterized_component: {total}",
-            id="parametrized-component",
-            data_value=total,
         ),
         html.hr(),
     )
@@ -53,7 +53,9 @@ def object_in_templatetag(my_object: TestObject):
     success = bool(my_object and my_object.value)
     co_name = inspect.currentframe().f_code.co_name  # type: ignore
     return html._(
-        html.div(f"{co_name}: ", str(my_object), id=co_name, data_success=success),
+        html.div(
+            {"id": co_name, "data-success": success}, f"{co_name}: ", str(my_object)
+        ),
         html.hr(),
     )
 
@@ -71,7 +73,7 @@ def object_in_templatetag(my_object: TestObject):
 def simple_button():
     return html._(
         "simple_button:",
-        SimpleButton(id="simple-button"),
+        SimpleButton({"id": "simple-button"}),
         html.hr(),
     )
 
@@ -87,7 +89,9 @@ def use_connection():
         and getattr(ws.carrier, "dotted_path", None)
     )
     return html.div(
-        f"use_connection: {ws}", html.hr(), id="use-connection", data_success=success
+        {"id": "use-connection", "data-success": success},
+        f"use_connection: {ws}",
+        html.hr(),
     )
 
 
@@ -96,7 +100,7 @@ def use_scope():
     scope = django_idom.hooks.use_scope()
     success = len(scope) >= 10 and scope["type"] == "websocket"
     return html.div(
-        f"use_scope: {scope}", html.hr(), id="use-scope", data_success=success
+        {"id": "use-scope", "data-success": success}, f"use_scope: {scope}", html.hr()
     )
 
 
@@ -105,7 +109,9 @@ def use_location():
     location = django_idom.hooks.use_location()
     success = bool(location)
     return html.div(
-        f"use_location: {location}", html.hr(), id="use-location", data_success=success
+        {"id": "use-location", "data-success": success},
+        f"use_location: {location}",
+        html.hr(),
     )
 
 
@@ -114,18 +120,20 @@ def use_origin():
     origin = django_idom.hooks.use_origin()
     success = bool(origin)
     return html.div(
-        f"use_origin: {origin}", html.hr(), id="use-origin", data_success=success
+        {"id": "use-origin", "data-success": success},
+        f"use_origin: {origin}",
+        html.hr(),
     )
 
 
 @component
 def django_css():
     return html.div(
+        {"id": "django-css"},
         django_idom.components.django_css("django-css-test.css", key="test"),
-        html.div("django_css: ", style={"display": "inline"}),
+        html.div({"style": {"display": "inline"}}, "django_css: "),
         html.button("This text should be blue."),
         html.hr(),
-        id="django-css",
     )
 
 
@@ -134,10 +142,9 @@ def django_js():
     success = False
     return html._(
         html.div(
+            {"id": "django-js", "data-success": success},
             f"django_js: {success}",
             django_idom.components.django_js("django-js-test.js", key="test"),
-            id="django-js",
-            data_success=success,
         ),
         html.hr(),
     )
@@ -146,22 +153,34 @@ def django_js():
 @component
 @django_idom.decorators.auth_required(
     fallback=html.div(
-        "unauthorized_user: Success", html.hr(), id="unauthorized-user-fallback"
+        {"id": "unauthorized-user-fallback"},
+        "unauthorized_user: Success",
+        html.hr(),
     )
 )
 def unauthorized_user():
-    return html.div("unauthorized_user: Fail", html.hr(), id="unauthorized-user")
+    return html.div(
+        {"id": "unauthorized-user"},
+        "unauthorized_user: Fail",
+        html.hr(),
+    )
 
 
 @component
 @django_idom.decorators.auth_required(
     auth_attribute="is_anonymous",
     fallback=html.div(
-        "authorized_user: Fail", html.hr(), id="authorized-user-fallback"
+        {"id": "authorized-user-fallback"},
+        "authorized_user: Fail",
+        html.hr(),
     ),
 )
 def authorized_user():
-    return html.div("authorized_user: Success", html.hr(), id="authorized-user")
+    return html.div(
+        {"id": "authorized-user"},
+        "authorized_user: Success",
+        html.hr(),
+    )
 
 
 def create_relational_parent() -> RelationalParent:
@@ -204,13 +223,15 @@ def relational_query():
     fk = foriegn_child.data.parent
 
     return html.div(
+        {
+            "id": "relational-query",
+            "data-success": bool(mtm) and bool(oto) and bool(mto) and bool(fk),
+        },
         html.div(f"Relational Parent Many To Many: {mtm}"),
         html.div(f"Relational Parent One To One: {oto}"),
         html.div(f"Relational Parent Many to One: {mto}"),
         html.div(f"Relational Child Foreign Key: {fk}"),
         html.hr(),
-        id="relational-query",
-        data_success=bool(mtm) and bool(oto) and bool(mto) and bool(fk),
     )
 
 
@@ -273,11 +294,13 @@ def on_change(event):
     return html.div(
         html.label("Add an item:"),
         html.input(
-            type="text",
-            id="todo-input",
-            value=input_value,
-            on_key_press=on_submit,
-            on_change=on_change,
+            {
+                "type": "text",
+                "id": "todo-input",
+                "value": input_value,
+                "on_key_press": on_submit,
+                "on_change": on_change,
+            }
         ),
         mutation_status,
         rendered_items,
@@ -289,15 +312,16 @@ def _render_todo_items(items, toggle_item):
     return html.ul(
         [
             html.li(
+                {"id": f"todo-item-{item.text}", "key": item.text},
                 item.text,
                 html.input(
-                    id=f"todo-item-{item.text}-checkbox",
-                    type="checkbox",
-                    checked=item.done,
-                    on_change=lambda event, i=item: toggle_item.execute(i),
+                    {
+                        "id": f"todo-item-{item.text}-checkbox",
+                        "type": "checkbox",
+                        "checked": item.done,
+                        "on_change": lambda event, i=item: toggle_item.execute(i),
+                    }
                 ),
-                key=item.text,
-                id=f"todo-item-{item.text}",
             )
             for item in items
         ]
@@ -335,45 +359,45 @@ def _render_todo_items(items, toggle_item):
 @component
 def view_to_component_sync_func_compatibility():
     return html.div(
+        {"id": inspect.currentframe().f_code.co_name},  # type: ignore
         _view_to_component_sync_func_compatibility(key="test"),
         html.hr(),
-        id=inspect.currentframe().f_code.co_name,  # type: ignore
     )
 
 
 @component
 def view_to_component_async_func_compatibility():
     return html.div(
+        {"id": inspect.currentframe().f_code.co_name},  # type: ignore
         _view_to_component_async_func_compatibility(),
         html.hr(),
-        id=inspect.currentframe().f_code.co_name,  # type: ignore
     )
 
 
 @component
 def view_to_component_sync_class_compatibility():
     return html.div(
+        {"id": inspect.currentframe().f_code.co_name},  # type: ignore
         _view_to_component_sync_class_compatibility(),
         html.hr(),
-        id=inspect.currentframe().f_code.co_name,  # type: ignore
     )
 
 
 @component
 def view_to_component_async_class_compatibility():
     return html.div(
+        {"id": inspect.currentframe().f_code.co_name},  # type: ignore
         _view_to_component_async_class_compatibility(),
         html.hr(),
-        id=inspect.currentframe().f_code.co_name,  # type: ignore
     )
 
 
 @component
 def view_to_component_template_view_class_compatibility():
     return html.div(
+        {"id": inspect.currentframe().f_code.co_name},  # type: ignore
         _view_to_component_template_view_class_compatibility(),
         html.hr(),
-        id=inspect.currentframe().f_code.co_name,  # type: ignore
     )
 
 
@@ -388,9 +412,11 @@ def on_click(_):
 
     return html._(
         html.button(
+            {
+                "id": f"{inspect.currentframe().f_code.co_name}_btn",  # type: ignore
+                "on_click": on_click,
+            },
             "Click me",
-            id=f"{inspect.currentframe().f_code.co_name}_btn",  # type: ignore
-            on_click=on_click,
         ),
         _view_to_component_request(request=request),
     )
@@ -405,9 +431,11 @@ def on_click(_):
 
     return html._(
         html.button(
+            {
+                "id": f"{inspect.currentframe().f_code.co_name}_btn",  # type: ignore
+                "on_click": on_click,
+            },
             "Click me",
-            id=f"{inspect.currentframe().f_code.co_name}_btn",  # type: ignore
-            on_click=on_click,
         ),
         _view_to_component_args(None, success),
     )
@@ -422,9 +450,11 @@ def on_click(_):
 
     return html._(
         html.button(
+            {
+                "id": f"{inspect.currentframe().f_code.co_name}_btn",  # type: ignore
+                "on_click": on_click,
+            },
             "Click me",
-            id=f"{inspect.currentframe().f_code.co_name}_btn",  # type: ignore
-            on_click=on_click,
         ),
         _view_to_component_kwargs(success=success),
     )
diff --git a/tests/test_app/tests/test_database.py b/tests/test_app/tests/test_database.py
index 3709de08..6a86c9b4 100644
--- a/tests/test_app/tests/test_database.py
+++ b/tests/test_app/tests/test_database.py
@@ -6,19 +6,19 @@
 from django.test import TransactionTestCase
 
 from django_idom import utils
-from django_idom.models import ComponentParams
+from django_idom.models import ComponentSession
 from django_idom.types import ComponentParamData
 
 
 class DatabaseTests(TransactionTestCase):
     def test_component_params(self):
         # Make sure the ComponentParams table is empty
-        self.assertEqual(ComponentParams.objects.count(), 0)
+        self.assertEqual(ComponentSession.objects.count(), 0)
         params_1 = self._save_params_to_db(1)
 
         # Check if a component params are in the database
-        self.assertEqual(ComponentParams.objects.count(), 1)
-        self.assertEqual(pickle.loads(ComponentParams.objects.first().data), params_1)  # type: ignore
+        self.assertEqual(ComponentSession.objects.count(), 1)
+        self.assertEqual(pickle.loads(ComponentSession.objects.first().params), params_1)  # type: ignore
 
         # Force `params_1` to expire
         from django_idom import config
@@ -28,18 +28,18 @@ def test_component_params(self):
 
         # Create a new, non-expired component params
         params_2 = self._save_params_to_db(2)
-        self.assertEqual(ComponentParams.objects.count(), 2)
+        self.assertEqual(ComponentSession.objects.count(), 2)
 
         # Delete the first component params based on expiration time
         utils.db_cleanup()  # Don't use `immediate` to test cache timestamping logic
 
         # Make sure `params_1` has expired
-        self.assertEqual(ComponentParams.objects.count(), 1)
-        self.assertEqual(pickle.loads(ComponentParams.objects.first().data), params_2)  # type: ignore
+        self.assertEqual(ComponentSession.objects.count(), 1)
+        self.assertEqual(pickle.loads(ComponentSession.objects.first().params), params_2)  # type: ignore
 
     def _save_params_to_db(self, value: Any) -> ComponentParamData:
         param_data = ComponentParamData((value,), {"test_value": value})
-        model = ComponentParams(uuid4().hex, data=pickle.dumps(param_data))
+        model = ComponentSession(uuid4().hex, params=pickle.dumps(param_data))
         model.full_clean()
         model.save()