Repository: reactive-python/reactpy-django (language: python)

Issue:
Set IDOM_DEBUG_MODE to settings.py:DEBUG
### Old Behavior

`IDOM_DEBUG_MODE` needed to be manually set

### New Behavior

Automatically set it based on `settings.py:DEBUG`

### Implementation Details

`IDOM_DEBUG_MODE = django.conf.settings.DEBUG`

### Code of Conduct

- [X] I agree to follow the [Code of Conduct](https://github.com/idom-team/django-idom/blob/main/CODE_OF_CONDUCT.md).


Discussion / hints:
Blocked on
- https://github.com/idom-team/idom/issues/582

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 62a202fc..9ac34c86 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,10 +22,35 @@ Using the following categories, list your changes in this order:
 
 ## [Unreleased]
 
+### 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`
+
+### Changed
+
+-   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`.
+-   It is now mandatory to run `manage.py migrate` after installing IDOM.
+-   Bumped the minimum IDOM version to 0.43.0
+
+### 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`
+
 ### Fixed
 
+-   `view_to_component` will now retain any HTML that was defined in a `<head>` tag.
+-   React client is now set to `production` rather than `development`.
 -   `use_query` will now utilize `field.related_name` when postprocessing many-to-one relationships
 
+### Security
+
+-   Fixed a potential method of component template tag argument spoofing.
+-   Exception information will no longer be displayed on the page, based on the value of `settings.py:DEBUG`.
+
 ## [2.2.1] - 2022-01-09
 
 ### Fixed
diff --git a/README.md b/README.md
index f45f1242..11e19477 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,7 @@ def hello_world(recipient: str):
 
 In your **Django app**'s HTML template, you can now embed your IDOM component using the `component` template tag. Within this tag, you will need to type in your dotted path to the component function as the first argument.
 
-Additionally, you can pass in keyword arguments into your component function. For example, after reading the code below, pay attention to how the function definition for `hello_world` (_in the previous example_) accepts a `recipient` argument.
+Additionally, you can pass in `args` and `kwargs` into your component function. For example, after reading the code below, pay attention to how the function definition for `hello_world` (_in the previous example_) accepts a `recipient` argument.
 
 <!--html-header-end-->
 <!--html-code-start-->
diff --git a/docs/src/contribute/code.md b/docs/src/contribute/code.md
index 2a6b9feb..3575671b 100644
--- a/docs/src/contribute/code.md
+++ b/docs/src/contribute/code.md
@@ -10,7 +10,7 @@ If you plan to make code changes to this repository, you will need to install th
 
 Once done, you should clone this repository:
 
-```bash
+```bash linenums="0"
 git clone https://github.com/idom-team/django-idom.git
 cd django-idom
 ```
@@ -20,13 +20,13 @@ Then, by running the command below you can:
 -   Install an editable version of the Python code
 -   Download, build, and install Javascript dependencies
 
-```bash
+```bash linenums="0"
 pip install -e . -r requirements.txt
 ```
 
 Finally, to verify that everything is working properly, you can manually run the development webserver.
 
-```bash
+```bash linenums="0"
 cd tests
 python manage.py runserver
 ```
diff --git a/docs/src/contribute/docs.md b/docs/src/contribute/docs.md
index d918c4b9..666cf6e1 100644
--- a/docs/src/contribute/docs.md
+++ b/docs/src/contribute/docs.md
@@ -5,7 +5,7 @@ If you plan to make changes to this documentation, you will need to install the
 
 Once done, you should clone this repository:
 
-```bash
+```bash linenums="0"
 git clone https://github.com/idom-team/django-idom.git
 cd django-idom
 ```
@@ -15,13 +15,13 @@ Then, by running the command below you can:
 -   Install an editable version of the documentation
 -   Self-host a test server for the documentation
 
-```bash
+```bash linenums="0"
 pip install -r ./requirements/build-docs.txt --upgrade
 ```
 
 Finally, to verify that everything is working properly, you can manually run the docs preview webserver.
 
-```bash
+```bash linenums="0"
 mkdocs serve
 ```
 
diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt
index 7892a120..c21b4dfd 100644
--- a/docs/src/dictionary.txt
+++ b/docs/src/dictionary.txt
@@ -25,3 +25,6 @@ idom
 asgi
 postfixed
 postprocessing
+serializable
+postprocessor
+preprocessor
diff --git a/docs/src/features/components.md b/docs/src/features/components.md
index 9f444d8d..849635ab 100644
--- a/docs/src/features/components.md
+++ b/docs/src/features/components.md
@@ -77,8 +77,7 @@ Convert any Django view into a IDOM component by using this decorator. Compatibl
 
     - Requires manual intervention to change request methods beyond `GET`.
     - IDOM events cannot conveniently be attached to converted view HTML.
-    - Does not currently load any HTML contained with a `<head>` tag
-    - Has no option to automatically intercept local anchor link (such as `#!html <a href='example/'></a>`) click events
+    - Has no option to automatically intercept local anchor link (such as `#!html <a href='example/'></a>`) click events.
 
     _Please note these limitations do not exist when using `compatibility` mode._
 
diff --git a/docs/src/features/decorators.md b/docs/src/features/decorators.md
index e643ec77..19ff8726 100644
--- a/docs/src/features/decorators.md
+++ b/docs/src/features/decorators.md
@@ -4,19 +4,16 @@
 
 ## Auth Required
 
-You can limit access to a component to users with a specific `auth_attribute` by using this decorator.
+You can limit access to a component to users with a specific `auth_attribute` by using this decorator (with or without parentheses).
 
 By default, this decorator checks if the user is logged in, and his/her account has not been deactivated.
 
 This decorator is commonly used to selectively render a component only if a user [`is_staff`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_staff) or [`is_superuser`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_superuser).
 
-This decorator can be used with or without parentheses.
-
 === "components.py"
 
     ```python
     from django_idom.decorators import auth_required
-    from django_idom.hooks import use_websocket
     from idom import component, html
 
     @component
@@ -70,7 +67,6 @@ This decorator can be used with or without parentheses.
 
         ```python
         from django_idom.decorators import auth_required
-        from django_idom.hooks import use_websocket
         from idom import component, html
 
         @component
@@ -87,7 +83,6 @@ This decorator can be used with or without parentheses.
 
         ```python
         from django_idom.decorators import auth_required
-        from django_idom.hooks import use_websocket
         from idom import component, html
 
 
@@ -120,7 +115,6 @@ This decorator can be used with or without parentheses.
 
         ```python
         from django_idom.decorators import auth_required
-        from django_idom.hooks import use_websocket
         from idom import component, html
 
         @component
diff --git a/docs/src/features/hooks.md b/docs/src/features/hooks.md
index 07040d0b..5e1f9731 100644
--- a/docs/src/features/hooks.md
+++ b/docs/src/features/hooks.md
@@ -399,20 +399,22 @@ The function you provide into this hook will have no return value.
 
     {% include-markdown "../../includes/orm.md" start="<!--orm-excp-start-->" end="<!--orm-excp-end-->" %}
 
-## Use Websocket
+## Use Origin
+
+This is a shortcut that returns the Websocket's `origin`.
 
-You can fetch the Django Channels [websocket](https://channels.readthedocs.io/en/stable/topics/consumers.html#asyncjsonwebsocketconsumer) at any time by using `use_websocket`.
+You can expect this hook to provide strings such as `http://example.com`.
 
 === "components.py"
 
     ```python
     from idom import component, html
-    from django_idom.hooks import use_websocket
+    from django_idom.hooks import use_origin
 
     @component
     def my_component():
-        my_websocket = use_websocket()
-        return html.div(my_websocket)
+        my_origin = use_origin()
+        return html.div(my_origin)
     ```
 
 ??? example "See Interface"
@@ -425,22 +427,22 @@ You can fetch the Django Channels [websocket](https://channels.readthedocs.io/en
 
     | Type | Description |
     | --- | --- |
-    | `IdomWebsocket` | The component's websocket. |
+    | `str | None` | A string containing the browser's current origin, obtained from websocket headers (if available). |
 
-## Use Scope
+## Use Connection
 
-This is a shortcut that returns the Websocket's [`scope`](https://channels.readthedocs.io/en/stable/topics/consumers.html#scope).
+You can fetch the Django Channels [websocket](https://channels.readthedocs.io/en/stable/topics/consumers.html#asyncjsonwebsocketconsumer) at any time by using `use_connection`.
 
 === "components.py"
 
     ```python
     from idom import component, html
-    from django_idom.hooks import use_scope
+    from django_idom.hooks import use_connection
 
     @component
     def my_component():
-        my_scope = use_scope()
-        return html.div(my_scope)
+        my_connection = use_connection()
+        return html.div(my_connection)
     ```
 
 ??? example "See Interface"
@@ -453,24 +455,22 @@ This is a shortcut that returns the Websocket's [`scope`](https://channels.readt
 
     | Type | Description |
     | --- | --- |
-    | `dict[str, Any]` | The websocket's `scope`. |
+    | `Connection` | The component's websocket. |
 
-## Use Location
-
-This is a shortcut that returns the Websocket's `path`.
+## Use Scope
 
-You can expect this hook to provide strings such as `/idom/my_path`.
+This is a shortcut that returns the Websocket's [`scope`](https://channels.readthedocs.io/en/stable/topics/consumers.html#scope).
 
 === "components.py"
 
     ```python
     from idom import component, html
-    from django_idom.hooks import use_location
+    from django_idom.hooks import use_scope
 
     @component
     def my_component():
-        my_location = use_location()
-        return html.div(my_location)
+        my_scope = use_scope()
+        return html.div(my_scope)
     ```
 
 ??? example "See Interface"
@@ -483,30 +483,24 @@ You can expect this hook to provide strings such as `/idom/my_path`.
 
     | Type | Description |
     | --- | --- |
-    | `Location` | A object containing the current URL's `pathname` and `search` query. |
+    | `MutableMapping[str, Any]` | The websocket's `scope`. |
 
-??? info "This hook's behavior will be changed in a future update"
-
-    This hook will be updated to return the browser's currently active path. This change will come in alongside IDOM URL routing support.
-
-    Check out [idom-team/idom-router#2](https://github.com/idom-team/idom-router/issues/2) for more information.
-
-## Use Origin
+## Use Location
 
-This is a shortcut that returns the Websocket's `origin`.
+This is a shortcut that returns the Websocket's `path`.
 
-You can expect this hook to provide strings such as `http://example.com`.
+You can expect this hook to provide strings such as `/idom/my_path`.
 
 === "components.py"
 
     ```python
     from idom import component, html
-    from django_idom.hooks import use_origin
+    from django_idom.hooks import use_location
 
     @component
     def my_component():
-        my_origin = use_origin()
-        return html.div(my_origin)
+        my_location = use_location()
+        return html.div(my_location)
     ```
 
 ??? example "See Interface"
@@ -519,4 +513,10 @@ You can expect this hook to provide strings such as `http://example.com`.
 
     | Type | Description |
     | --- | --- |
-    | `str | None` | A string containing the browser's current origin, obtained from websocket headers (if available). |
+    | `Location` | A object containing the current URL's `pathname` and `search` query. |
+
+??? info "This hook's behavior will be changed in a future update"
+
+    This hook will be updated to return the browser's currently active path. This change will come in alongside IDOM URL routing support.
+
+    Check out [idom-team/idom-router#2](https://github.com/idom-team/idom-router/issues/2) for more information.
diff --git a/docs/src/features/settings.md b/docs/src/features/settings.md
index 4eb83fe1..826da81d 100644
--- a/docs/src/features/settings.md
+++ b/docs/src/features/settings.md
@@ -9,16 +9,17 @@
     <!--settings-start-->
 
     ```python
-    # If "idom" cache is not configured, then we will use "default" instead
+    # If "idom" cache is not configured, then "default" will be used
+    # IDOM expects a multiprocessing-safe and thread-safe cache backend.
     CACHES = {
     "idom": {"BACKEND": ...},
     }
 
-    # Maximum seconds between two reconnection attempts that would cause the client give up.
-    # 0 will disable reconnection.
-    IDOM_WS_MAX_RECONNECT_TIMEOUT = 604800
+    # Maximum seconds between reconnection attempts before giving up.
+    # Use `0` to prevent component reconnection.
+    IDOM_RECONNECT_MAX = 259200
 
-    # The URL for IDOM to serve websockets
+    # The URL for IDOM to serve the component rendering websocket
     IDOM_WEBSOCKET_URL = "idom/"
 
     # Dotted path to the default postprocessor function, or `None`
diff --git a/docs/src/features/templatetag.md b/docs/src/features/templatetag.md
index 6aa58023..7dc34c2e 100644
--- a/docs/src/features/templatetag.md
+++ b/docs/src/features/templatetag.md
@@ -4,6 +4,8 @@
 
 ## Component
 
+The `component` template tag can be used to insert any number of IDOM components onto your page.
+
 === "my-template.html"
 
     {% include-markdown "../../../README.md" start="<!--html-code-start-->" end="<!--html-code-end-->" %}
@@ -12,7 +14,7 @@
 
 ??? warning "Do not use context variables for the IDOM component name"
 
-    Our pre-processor relies on the template tag containing a string.
+    Our preprocessor relies on the template tag containing a string.
 
     **Do not** use Django template/context variables for the component path. Failure to follow this warning will result in unexpected behavior.
 
@@ -44,7 +46,7 @@
     For this template tag, there are two reserved keyword arguments: `class` and `key`
 
     -   `class` allows you to apply a HTML class to the top-level component div. This is useful for styling purposes.
-    -   `key` allows you to force the component to use a [specific key value](https://idom-docs.herokuapp.com/docs/guides/understanding-idom/why-idom-needs-keys.html?highlight=key). You typically won't need to set this.
+    -   `key` allows you to force the component to use a [specific key value](https://idom-docs.herokuapp.com/docs/guides/creating-interfaces/rendering-data/index.html#organizing-items-with-keys). Using `key` within a template tag is effectively useless.
 
     === "my-template.html"
 
@@ -63,7 +65,8 @@
     | Name | Type | Description | Default |
     | --- | --- | --- | --- |
     | `dotted_path` | `str` | The dotted path to the component to render. | N/A |
-    | `**kwargs` | `Any` | The keyword arguments to pass to the component. | N/A |
+    | `*args` | `Any` | The positional arguments to provide to the component. | N/A |
+    | `**kwargs` | `Any` | The keyword arguments to provide to the component. | N/A |
 
     <font size="4">**Returns**</font>
 
@@ -91,28 +94,8 @@
         </html>
         ```
 
-    But keep in mind, in scenarios where you are trying to create a Single Page Application (SPA) within Django, you will only have one central component within your `#!html <body>` tag.
+    Please note that components separated like this will not be able to interact with each other, except through database queries.
 
-    Additionally, the components in the example above will not be able to interact with each other, except through database queries.
+    Additionally, in scenarios where you are trying to create a Single Page Application (SPA) within Django, you will only have one component within your `#!html <body>` tag.
 
 <!--multiple-components-end-->
-<!--kwargs-start-->
-
-??? question "Can I use positional arguments instead of keyword arguments?"
-
-    You can only pass in **keyword arguments** within the template tag. Due to technical limitations, **positional arguments** are not supported at this time.
-
-<!--kwargs-end-->
-<!--tags-start-->
-
-??? question "What is a "template tag"?"
-
-    You can think of template tags as Django's way of allowing you to run Python code within your HTML. Django-IDOM uses a `#!jinja {% component ... %}` template tag to perform it's magic.
-
-    Keep in mind, in order to use the `#!jinja {% component ... %}` tag, you will need to first call `#!jinja {% load idom %}` to gain access to it.
-
-    === "my-template.html"
-
-        {% include-markdown "../../../README.md" start="<!--html-code-start-->" end="<!--html-code-end-->" %}
-
-<!--tags-end-->
diff --git a/docs/src/features/utils.md b/docs/src/features/utils.md
new file mode 100644
index 00000000..9a0aca8b
--- /dev/null
+++ b/docs/src/features/utils.md
@@ -0,0 +1,60 @@
+???+ summary
+
+    Utility functions that you can use when needed.
+
+## Django Query Postprocessor
+
+This is the default postprocessor for the `use_query` hook.
+
+This postprocessor is designed to prevent Django's `SynchronousOnlyException` by recursively fetching all fields within a `Model` or `QuerySet` to prevent [lazy execution](https://docs.djangoproject.com/en/dev/topics/db/queries/#querysets-are-lazy).
+
+=== "components.py"
+
+    ```python
+    from example_project.my_app.models import TodoItem
+    from idom import component
+    from django_idom.hooks import use_query
+    from django_idom.types import QueryOptions
+    from django_idom.utils import django_query_postprocessor
+
+    def get_items():
+        return TodoItem.objects.all()
+
+    @component
+    def todo_list():
+        # These `QueryOptions` are functionally equivalent to Django-IDOM's default values
+        item_query = use_query(
+            QueryOptions(
+                postprocessor=django_query_postprocessor,
+                postprocessor_kwargs={"many_to_many": True, "many_to_one": True},
+            ),
+            get_items,
+        )
+
+        ...
+    ```
+
+=== "models.py"
+
+    ```python
+    from django.db import models
+
+    class TodoItem(models.Model):
+        text = models.CharField(max_length=255)
+    ```
+
+??? example "See Interface"
+
+    <font size="4">**Parameters**</font>
+
+    | Name | Type | Description | Default |
+    | --- | --- | --- | --- |
+    | `data` | `QuerySet | Model` | The `Model` or `QuerySet` to recursively fetch fields from. | N/A |
+    | `many_to_many` | `bool` | Whether or not to recursively fetch `ManyToManyField` relationships. | `True` |
+    | `many_to_one` | `bool` | Whether or not to recursively fetch `ForeignKey` relationships. | `True` |
+
+    <font size="4">**Returns**</font>
+
+    | Type | Description |
+    | --- | --- |
+    | `QuerySet | Model` | The `Model` or `QuerySet` with all fields fetched. |
diff --git a/docs/src/getting-started/installation.md b/docs/src/getting-started/installation.md
index 1224df32..f311abef 100644
--- a/docs/src/getting-started/installation.md
+++ b/docs/src/getting-started/installation.md
@@ -2,13 +2,13 @@
 
     Django-IDOM can be installed from PyPI to an existing **Django project** with minimal configuration.
 
-## Step 0: Set up a Django Project
+## Step 0: Create a Django Project
 
 These docs assumes you have already created [a **Django project**](https://docs.djangoproject.com/en/dev/intro/tutorial01/), which involves creating and installing at least one **Django app**. If not, check out this [9 minute YouTube tutorial](https://www.youtube.com/watch?v=ZsJRXS_vrw0) created by _IDG TECHtalk_.
 
 ## Step 1: Install from PyPI
 
-```bash
+```bash linenums="0"
 pip install django-idom
 ```
 
@@ -96,3 +96,11 @@ Register IDOM's Websocket using `IDOM_WEBSOCKET_PATH`.
 ??? question "Where is my `asgi.py`?"
 
     If you do not have an `asgi.py`, follow the [`channels` installation guide](https://channels.readthedocs.io/en/stable/installation.html).
+
+## Step 5: Run Migrations
+
+Run Django's database migrations to initialize Django-IDOM's database table.
+
+```bash linenums="0"
+python manage.py migrate
+```
diff --git a/docs/src/getting-started/reference-component.md b/docs/src/getting-started/reference-component.md
index f7c03ecb..1576f3ee 100644
--- a/docs/src/getting-started/reference-component.md
+++ b/docs/src/getting-started/reference-component.md
@@ -16,10 +16,6 @@
 
 {% include-markdown "../features/templatetag.md" start="<!--multiple-components-start-->" end="<!--multiple-components-end-->" %}
 
-{% include-markdown "../features/templatetag.md" start="<!--kwargs-start-->" end="<!--kwargs-end-->" %}
-
-{% include-markdown "../features/templatetag.md" start="<!--tags-start-->" end="<!--tags-end-->" %}
-
 ??? question "Where is my templates folder?"
 
        If you do not have a `templates` folder in your **Django app**, you can simply create one! Keep in mind, templates within this folder will not be detected by Django unless you [add the corresponding **Django app** to `settings.py:INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/applications/#configuring-applications).
diff --git a/mkdocs.yml b/mkdocs.yml
index f51bc051..2a60cb5c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -12,6 +12,7 @@ nav:
           - Components: features/components.md
           - Hooks: features/hooks.md
           - Decorators: features/decorators.md
+          - Utilities: features/utils.md
           - Template Tag: features/templatetag.md
           - Settings: features/settings.md
     - Contribute:
diff --git a/pyproject.toml b/pyproject.toml
index a8a2fb0a..c9321d8a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,11 +10,15 @@ ensure_newline_before_comments = "True"
 include_trailing_comma = "True"
 line_length = 88
 lines_after_imports = 2
-extend_skip_glob = ["*/migrations/*"]
+extend_skip_glob = ["*/migrations/*", '.nox/*', '.venv/*', 'build/*']
 
 [tool.mypy]
+exclude = [
+    'migrations/.*',
+]
 ignore_missing_imports = true
 warn_unused_configs = true
 warn_redundant_casts = true
 warn_unused_ignores = true
 check_untyped_defs = true
+incremental = false
diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt
index d2329eb6..441e7a04 100644
--- a/requirements/pkg-deps.txt
+++ b/requirements/pkg-deps.txt
@@ -1,4 +1,5 @@
 channels >=4.0.0
-idom >=0.40.2, <0.41.0
+idom >=0.43.0, <0.44.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 c1991661..ca378289 100644
--- a/src/django_idom/__init__.py
+++ b/src/django_idom/__init__.py
@@ -1,12 +1,10 @@
 from django_idom import components, decorators, hooks, types, utils
-from django_idom.types import IdomWebsocket
 from django_idom.websocket.paths import IDOM_WEBSOCKET_PATH
 
 
 __version__ = "2.2.1"
 __all__ = [
     "IDOM_WEBSOCKET_PATH",
-    "IdomWebsocket",
     "hooks",
     "components",
     "decorators",
diff --git a/src/django_idom/apps.py b/src/django_idom/apps.py
index 6a963664..f91b50b7 100644
--- a/src/django_idom/apps.py
+++ b/src/django_idom/apps.py
@@ -1,6 +1,12 @@
+import logging
+
 from django.apps import AppConfig
+from django.db.utils import OperationalError
+
+from django_idom.utils import ComponentPreloader, db_cleanup
 
-from django_idom.utils import ComponentPreloader
+
+_logger = logging.getLogger(__name__)
 
 
 class DjangoIdomConfig(AppConfig):
@@ -8,4 +14,13 @@ class DjangoIdomConfig(AppConfig):
 
     def ready(self):
         # Populate the IDOM component registry when Django is ready
-        ComponentPreloader().register_all()
+        ComponentPreloader().run()
+
+        # Delete expired database entries
+        # Suppress exceptions to avoid issues with `manage.py` commands such as
+        # `test`, `migrate`, `makemigrations`, or any custom user created commands
+        # 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...")
diff --git a/src/django_idom/components.py b/src/django_idom/components.py
index 163eb47b..fd9d5292 100644
--- a/src/django_idom/components.py
+++ b/src/django_idom/components.py
@@ -11,9 +11,8 @@
 from idom import component, hooks, html, utils
 from idom.types import ComponentType, Key, VdomDict
 
-from django_idom.config import IDOM_CACHE, IDOM_VIEW_COMPONENT_IFRAMES
 from django_idom.types import ViewComponentIframe
-from django_idom.utils import _generate_obj_name, render_view
+from django_idom.utils import generate_obj_name, render_view
 
 
 class _ViewComponentConstructor(Protocol):
@@ -33,6 +32,8 @@ def _view_to_component(
     args: Sequence | None,
     kwargs: dict | None,
 ):
+    from django_idom.config import IDOM_VIEW_COMPONENT_IFRAMES
+
     converted_view, set_converted_view = hooks.use_state(
         cast(Union[VdomDict, None], None)
     )
@@ -47,8 +48,8 @@ def _view_to_component(
     # Render the view render within a hook
     @hooks.use_effect(
         dependencies=[
-            json.dumps(vars(_request), default=lambda x: _generate_obj_name(x)),
-            json.dumps([_args, _kwargs], default=lambda x: _generate_obj_name(x)),
+            json.dumps(vars(_request), default=lambda x: generate_obj_name(x)),
+            json.dumps([_args, _kwargs], default=lambda x: generate_obj_name(x)),
         ]
     )
     async def async_render():
@@ -62,6 +63,7 @@ async def async_render():
         set_converted_view(
             utils.html_to_vdom(
                 response.content.decode("utf-8").strip(),
+                utils.del_html_head_body_transform,
                 *transforms,
                 strict=strict_parsing,
             )
@@ -130,7 +132,7 @@ def view_to_component(
             perfectly adhere to HTML5.
 
     Returns:
-        Callable: A function that takes `request: HttpRequest | None, *args: Any, key: Key | None, **kwargs: Any`
+        A function that takes `request: HttpRequest | None, *args: Any, key: Key | None, **kwargs: Any`
             and returns an IDOM component.
     """
 
@@ -197,6 +199,8 @@ def django_js(static_path: str, key: Key | None = None):
 
 
 def _cached_static_contents(static_path: str):
+    from django_idom.config import IDOM_CACHE
+
     # Try to find the file within Django's static files
     abs_path = find(static_path)
     if not abs_path:
diff --git a/src/django_idom/config.py b/src/django_idom/config.py
index 0e1d4cd7..a7b4ca8d 100644
--- a/src/django_idom/config.py
+++ b/src/django_idom/config.py
@@ -4,12 +4,14 @@
 
 from django.conf import settings
 from django.core.cache import DEFAULT_CACHE_ALIAS, BaseCache, caches
+from idom.config import IDOM_DEBUG_MODE
 from idom.core.types import ComponentConstructor
 
-from django_idom.defaults import _DEFAULT_QUERY_POSTPROCESSOR
 from django_idom.types import Postprocessor, ViewComponentIframe
+from django_idom.utils import import_dotted_path
 
 
+IDOM_DEBUG_MODE.set_current(getattr(settings, "DEBUG"))
 IDOM_REGISTERED_COMPONENTS: Dict[str, ComponentConstructor] = {}
 IDOM_VIEW_COMPONENT_IFRAMES: Dict[str, ViewComponentIframe] = {}
 IDOM_WEBSOCKET_URL = getattr(
@@ -17,14 +19,20 @@
     "IDOM_WEBSOCKET_URL",
     "idom/",
 )
-IDOM_WS_MAX_RECONNECT_TIMEOUT = getattr(
+IDOM_RECONNECT_MAX = getattr(
     settings,
-    "IDOM_WS_MAX_RECONNECT_TIMEOUT",
-    604800,
+    "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_DEFAULT_QUERY_POSTPROCESSOR: Postprocessor | None = _DEFAULT_QUERY_POSTPROCESSOR
+IDOM_DEFAULT_QUERY_POSTPROCESSOR: Postprocessor | None = import_dotted_path(
+    getattr(
+        settings,
+        "IDOM_DEFAULT_QUERY_POSTPROCESSOR",
+        "django_idom.utils.django_query_postprocessor",
+    )
+)
diff --git a/src/django_idom/decorators.py b/src/django_idom/decorators.py
index 6d01c990..fb78f74e 100644
--- a/src/django_idom/decorators.py
+++ b/src/django_idom/decorators.py
@@ -5,7 +5,7 @@
 
 from idom.core.types import ComponentType, VdomDict
 
-from django_idom.hooks import use_websocket
+from django_idom.hooks import use_scope
 
 
 def auth_required(
@@ -27,9 +27,9 @@ def auth_required(
     def decorator(component):
         @wraps(component)
         def _wrapped_func(*args, **kwargs):
-            websocket = use_websocket()
+            scope = use_scope()
 
-            if getattr(websocket.scope["user"], auth_attribute):
+            if getattr(scope["user"], auth_attribute):
                 return component(*args, **kwargs)
             return fallback(*args, **kwargs) if callable(fallback) else fallback
 
diff --git a/src/django_idom/defaults.py b/src/django_idom/defaults.py
deleted file mode 100644
index bc0530bb..00000000
--- a/src/django_idom/defaults.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from __future__ import annotations
-
-from typing import Any, Callable
-
-from django.conf import settings
-
-from django_idom.utils import _import_dotted_path
-
-
-_DEFAULT_QUERY_POSTPROCESSOR: Callable[..., Any] | None = _import_dotted_path(
-    getattr(
-        settings,
-        "IDOM_DEFAULT_QUERY_POSTPROCESSOR",
-        "django_idom.utils.django_query_postprocessor",
-    )
-)
diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py
index 4455f779..dd17ca16 100644
--- a/src/django_idom/hooks.py
+++ b/src/django_idom/hooks.py
@@ -7,6 +7,7 @@
     Awaitable,
     Callable,
     DefaultDict,
+    MutableMapping,
     Sequence,
     Union,
     cast,
@@ -15,18 +16,21 @@
 
 from channels.db import database_sync_to_async as _database_sync_to_async
 from idom import use_callback, use_ref
+from idom.backend.hooks import use_connection as _use_connection
+from idom.backend.hooks import use_location as _use_location
+from idom.backend.hooks import use_scope as _use_scope
 from idom.backend.types import Location
-from idom.core.hooks import Context, create_context, use_context, use_effect, use_state
+from idom.core.hooks import use_effect, use_state
 
 from django_idom.types import (
-    IdomWebsocket,
+    Connection,
     Mutation,
     Query,
     QueryOptions,
     _Params,
     _Result,
 )
-from django_idom.utils import _generate_obj_name
+from django_idom.utils import generate_obj_name
 
 
 _logger = logging.getLogger(__name__)
@@ -34,7 +38,6 @@
     Callable[..., Callable[..., Awaitable[Any]]],
     _database_sync_to_async,
 )
-WebsocketContext: Context[IdomWebsocket | None] = create_context(None)
 _REFETCH_CALLBACKS: DefaultDict[
     Callable[..., Any], set[Callable[[], None]]
 ] = DefaultDict(set)
@@ -42,16 +45,13 @@
 
 def use_location() -> Location:
     """Get the current route as a `Location` object"""
-    # TODO: Use the browser's current page, rather than the WS route
-    scope = use_scope()
-    search = scope["query_string"].decode()
-    return Location(scope["path"], f"?{search}" if search else "")
+    return _use_location()
 
 
 def use_origin() -> str | None:
     """Get the current origin as a string. If the browser did not send an origin header,
     this will be None."""
-    scope = use_scope()
+    scope = _use_scope()
     try:
         return next(
             (
@@ -65,17 +65,14 @@ def use_origin() -> str | None:
         return None
 
 
-def use_scope() -> dict[str, Any]:
+def use_scope() -> MutableMapping[str, Any]:
     """Get the current ASGI scope dictionary"""
-    return use_websocket().scope
+    return _use_scope()
 
 
-def use_websocket() -> IdomWebsocket:
-    """Get the current IdomWebsocket object"""
-    websocket = use_context(WebsocketContext)
-    if websocket is None:
-        raise RuntimeError("No websocket. Are you running with a Django server?")
-    return websocket
+def use_connection() -> Connection:
+    """Get the current `Connection` object"""
+    return _use_connection()
 
 
 @overload
@@ -165,7 +162,7 @@ def execute_query() -> None:
             set_loading(False)
             set_error(e)
             _logger.exception(
-                f"Failed to execute query: {_generate_obj_name(query) or query}"
+                f"Failed to execute query: {generate_obj_name(query) or query}"
             )
             return
         finally:
@@ -208,7 +205,7 @@ def execute_mutation() -> None:
                 set_loading(False)
                 set_error(e)
                 _logger.exception(
-                    f"Failed to execute mutation: {_generate_obj_name(mutate) or mutate}"
+                    f"Failed to execute mutation: {generate_obj_name(mutate) or mutate}"
                 )
             else:
                 set_loading(False)
diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py
index 57233bab..d8858f4c 100644
--- a/src/django_idom/http/views.py
+++ b/src/django_idom/http/views.py
@@ -5,13 +5,14 @@
 from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
 from idom.config import IDOM_WED_MODULES_DIR
 
-from django_idom.config import IDOM_CACHE, IDOM_VIEW_COMPONENT_IFRAMES
-from django_idom.utils import render_view
+from django_idom.utils import create_cache_key, render_view
 
 
 async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse:
     """Gets JavaScript required for IDOM modules at runtime. These modules are
     returned from cache if available."""
+    from django_idom.config import IDOM_CACHE
+
     web_modules_dir = IDOM_WED_MODULES_DIR.current
     path = os.path.abspath(web_modules_dir.joinpath(*file.split("/")))
 
@@ -23,7 +24,7 @@ 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 = f"django_idom:web_module:{str(path).lstrip(str(web_modules_dir))}"
+    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))
     if response is None:
         async with async_open(path, "r") as fp:
@@ -40,6 +41,8 @@ async def view_to_component_iframe(
 ) -> HttpResponse:
     """Returns a view that was registered by view_to_component.
     This view is intended to be used as iframe, for compatibility purposes."""
+    from django_idom.config import IDOM_VIEW_COMPONENT_IFRAMES
+
     # Get the view from IDOM_REGISTERED_IFRAMES
     iframe = IDOM_VIEW_COMPONENT_IFRAMES.get(view_path)
     if not iframe:
diff --git a/src/django_idom/migrations/0001_initial.py b/src/django_idom/migrations/0001_initial.py
new file mode 100644
index 00000000..3ad19d56
--- /dev/null
+++ b/src/django_idom/migrations/0001_initial.py
@@ -0,0 +1,26 @@
+# Generated by Django 4.1.3 on 2023-01-10 00:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = []
+
+    operations = [
+        migrations.CreateModel(
+            name="ComponentParams",
+            fields=[
+                (
+                    "uuid",
+                    models.UUIDField(
+                        editable=False, primary_key=True, serialize=False, unique=True
+                    ),
+                ),
+                ("data", models.BinaryField()),
+                ("created_at", models.DateTimeField(auto_now_add=True)),
+            ],
+        ),
+    ]
diff --git a/src/django_idom/migrations/0002_rename_created_at_componentparams_last_accessed.py b/src/django_idom/migrations/0002_rename_created_at_componentparams_last_accessed.py
new file mode 100644
index 00000000..80207bce
--- /dev/null
+++ b/src/django_idom/migrations/0002_rename_created_at_componentparams_last_accessed.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.5 on 2023-01-11 01:55
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("django_idom", "0001_initial"),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name="componentparams",
+            old_name="created_at",
+            new_name="last_accessed",
+        ),
+    ]
diff --git a/src/django_idom/migrations/__init__.py b/src/django_idom/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/django_idom/models.py b/src/django_idom/models.py
new file mode 100644
index 00000000..1e67f368
--- /dev/null
+++ b/src/django_idom/models.py
@@ -0,0 +1,7 @@
+from django.db import models
+
+
+class ComponentParams(models.Model):
+    uuid = models.UUIDField(primary_key=True, editable=False, unique=True)  # type: ignore
+    data = models.BinaryField(editable=False)  # type: ignore
+    last_accessed = models.DateTimeField(auto_now_add=True)  # type: ignore
diff --git a/src/django_idom/templates/idom/component.html b/src/django_idom/templates/idom/component.html
index fc8ba6f8..7fd82c22 100644
--- a/src/django_idom/templates/idom/component.html
+++ b/src/django_idom/templates/idom/component.html
@@ -2,13 +2,12 @@
 <div id="{{ idom_mount_uuid }}" class="{{ class }}"></div>
 <script type="module" crossorigin="anonymous">
   import { mountViewToElement } from "{% static 'django_idom/client.js' %}";
-  const mountPoint = document.getElementById("{{ idom_mount_uuid }}");
+  const mountElement = document.getElementById("{{ idom_mount_uuid }}");
   mountViewToElement(
-    mountPoint,
+    mountElement,
     "{{ idom_websocket_url }}",
     "{{ idom_web_modules_url }}",
-    "{{ idom_ws_max_reconnect_timeout }}",
-    "{{ idom_component_id }}",
-    "{{ idom_component_params }}"
+    "{{ idom_reconnect_max }}",
+    "{{ idom_component_path }}",
   );
 </script>
diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py
index b6bb6868..4309177b 100644
--- a/src/django_idom/templatetags/idom.py
+++ b/src/django_idom/templatetags/idom.py
@@ -1,13 +1,13 @@
-import json
-from typing import Any
-from urllib.parse import urlencode
 from uuid import uuid4
 
+import dill as pickle
 from django import template
 from django.urls import reverse
 
-from django_idom.config import IDOM_WEBSOCKET_URL, IDOM_WS_MAX_RECONNECT_TIMEOUT
-from django_idom.utils import _register_component
+from django_idom import models
+from django_idom.config import IDOM_RECONNECT_MAX, IDOM_WEBSOCKET_URL
+from django_idom.types import ComponentParamData
+from django_idom.utils import _register_component, func_has_params
 
 
 IDOM_WEB_MODULES_URL = reverse("idom:web_modules", args=["x"])[:-1][1:]
@@ -15,14 +15,15 @@
 
 
 @register.inclusion_tag("idom/component.html")
-def component(dotted_path: str, **kwargs: Any):
+def component(dotted_path: str, *args, **kwargs):
     """This tag is used to embed an existing IDOM component into your HTML template.
 
     Args:
         dotted_path: The dotted path to the component to render.
+        *args: The positional arguments to provide to the component.
 
     Keyword Args:
-        **kwargs: The keyword arguments to pass to the component.
+        **kwargs: The keyword arguments to provide to the component.
 
     Example ::
 
@@ -34,17 +35,29 @@ def component(dotted_path: str, **kwargs: Any):
         </body>
         </html>
     """
-    _register_component(dotted_path)
-
+    component = _register_component(dotted_path)
+    uuid = uuid4().hex
     class_ = kwargs.pop("class", "")
-    json_kwargs = json.dumps(kwargs, separators=(",", ":"))
+    kwargs.pop("key", "")  # `key` is effectively useless for the root node
+
+    # Store the component's args/kwargs in the database if needed
+    # This will be fetched by the websocket consumer later
+    try:
+        if func_has_params(component, *args, **kwargs):
+            params = ComponentParamData(args, kwargs)
+            model = models.ComponentParams(uuid=uuid, data=pickle.dumps(params))
+            model.full_clean()
+            model.save()
+    except TypeError as e:
+        raise TypeError(
+            f"The provided parameters are incompatible with component '{dotted_path}'."
+        ) from e
 
     return {
         "class": class_,
         "idom_websocket_url": IDOM_WEBSOCKET_URL,
         "idom_web_modules_url": IDOM_WEB_MODULES_URL,
-        "idom_ws_max_reconnect_timeout": IDOM_WS_MAX_RECONNECT_TIMEOUT,
-        "idom_mount_uuid": uuid4().hex,
-        "idom_component_id": dotted_path,
-        "idom_component_params": urlencode({"kwargs": json_kwargs}),
+        "idom_reconnect_max": IDOM_RECONNECT_MAX,
+        "idom_mount_uuid": uuid,
+        "idom_component_path": f"{dotted_path}/{uuid}/",
     }
diff --git a/src/django_idom/types.py b/src/django_idom/types.py
index 7705c98b..1427d62a 100644
--- a/src/django_idom/types.py
+++ b/src/django_idom/types.py
@@ -6,6 +6,7 @@
     Awaitable,
     Callable,
     Generic,
+    MutableMapping,
     Optional,
     Protocol,
     Sequence,
@@ -16,12 +17,23 @@
 from django.db.models.base import Model
 from django.db.models.query import QuerySet
 from django.views.generic import View
+from idom.types import Connection as _Connection
 from typing_extensions import ParamSpec
 
-from django_idom.defaults import _DEFAULT_QUERY_POSTPROCESSOR
 
-
-__all__ = ["_Result", "_Params", "_Data", "IdomWebsocket", "Query", "Mutation"]
+__all__ = [
+    "_Result",
+    "_Params",
+    "_Data",
+    "ComponentWebsocket",
+    "Query",
+    "Mutation",
+    "Connection",
+    "ViewComponentIframe",
+    "Postprocessor",
+    "QueryOptions",
+    "ComponentParamData",
+]
 
 _Result = TypeVar("_Result", bound=Union[Model, QuerySet[Any]])
 _Params = ParamSpec("_Params")
@@ -29,13 +41,15 @@
 
 
 @dataclass
-class IdomWebsocket:
-    """Websocket returned by the `use_websocket` hook."""
+class ComponentWebsocket:
+    """Carrier type for the `use_connection` hook."""
 
-    scope: dict
     close: Callable[[Optional[int]], Awaitable[None]]
     disconnect: Callable[[int], Awaitable[None]]
-    view_id: str
+    dotted_path: str
+
+
+Connection = _Connection[ComponentWebsocket]
 
 
 @dataclass
@@ -74,7 +88,9 @@ def __call__(self, data: Any) -> Any:
 class QueryOptions:
     """Configuration options that can be provided to `use_query`."""
 
-    postprocessor: Postprocessor | None = _DEFAULT_QUERY_POSTPROCESSOR
+    from django_idom.config import IDOM_DEFAULT_QUERY_POSTPROCESSOR
+
+    postprocessor: Postprocessor | None = IDOM_DEFAULT_QUERY_POSTPROCESSOR
     """A callable that can modify the query `data` after the query has been executed.
 
     The first argument of postprocessor must be the query `data`. All proceeding arguments
@@ -87,5 +103,14 @@ class QueryOptions:
     additionally can be configured via `postprocessor_kwargs` to recursively fetch
     `many_to_many` and `many_to_one` fields."""
 
-    postprocessor_kwargs: dict[str, Any] = field(default_factory=lambda: {})
+    postprocessor_kwargs: MutableMapping[str, Any] = field(default_factory=lambda: {})
     """Keyworded arguments directly passed into the `postprocessor` for configuration."""
+
+
+@dataclass
+class ComponentParamData:
+    """Container used for serializing component parameters.
+    This dataclass is pickled & stored in the database, then unpickled when needed."""
+
+    args: Sequence
+    kwargs: MutableMapping[str, Any]
diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py
index 52a4eed0..a0963b35 100644
--- a/src/django_idom/utils.py
+++ b/src/django_idom/utils.py
@@ -1,9 +1,11 @@
 from __future__ import annotations
 
 import contextlib
+import inspect
 import logging
 import os
 import re
+from datetime import datetime, timedelta
 from fnmatch import fnmatch
 from importlib import import_module
 from inspect import iscoroutinefunction
@@ -16,6 +18,7 @@
 from django.db.models.query import QuerySet
 from django.http import HttpRequest, HttpResponse
 from django.template import engines
+from django.utils import timezone
 from django.utils.encoding import smart_str
 from django.views import View
 
@@ -34,6 +37,7 @@
     + _component_kwargs
     + r"\s*%}"
 )
+DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
 
 
 async def render_view(
@@ -74,17 +78,21 @@ async def render_view(
     return response
 
 
-def _register_component(dotted_path: str) -> None:
+def _register_component(dotted_path: str) -> Callable:
+    """Adds a component to the mapping of registered components.
+    This should only be called on startup to maintain synchronization during mulitprocessing.
+    """
     from django_idom.config import IDOM_REGISTERED_COMPONENTS
 
     if dotted_path in IDOM_REGISTERED_COMPONENTS:
-        return
+        return IDOM_REGISTERED_COMPONENTS[dotted_path]
 
-    IDOM_REGISTERED_COMPONENTS[dotted_path] = _import_dotted_path(dotted_path)
+    IDOM_REGISTERED_COMPONENTS[dotted_path] = import_dotted_path(dotted_path)
     _logger.debug("IDOM has registered component %s", dotted_path)
+    return IDOM_REGISTERED_COMPONENTS[dotted_path]
 
 
-def _import_dotted_path(dotted_path: str) -> Callable:
+def import_dotted_path(dotted_path: str) -> Callable:
     """Imports a dotted path and returns the callable."""
     module_name, component_name = dotted_path.rsplit(".", 1)
 
@@ -99,24 +107,28 @@ def _import_dotted_path(dotted_path: str) -> Callable:
 
 
 class ComponentPreloader:
-    def register_all(self):
+    """Preloads all IDOM components found within Django templates.
+    This should only be `run` once on startup to maintain synchronization during mulitprocessing.
+    """
+
+    def run(self):
         """Registers all IDOM components found within Django templates."""
         # Get all template folder paths
-        paths = self._get_paths()
+        paths = self.get_paths()
         # Get all HTML template files
-        templates = self._get_templates(paths)
+        templates = self.get_templates(paths)
         # Get all components
-        components = self._get_components(templates)
+        components = self.get_components(templates)
         # Register all components
-        self._register_components(components)
+        self.register_components(components)
 
-    def _get_loaders(self):
+    def get_loaders(self):
         """Obtains currently configured template loaders."""
         template_source_loaders = []
         for e in engines.all():
             if hasattr(e, "engine"):
                 template_source_loaders.extend(
-                    e.engine.get_template_loaders(e.engine.loaders)  # type: ignore
+                    e.engine.get_template_loaders(e.engine.loaders)
                 )
         loaders = []
         for loader in template_source_loaders:
@@ -126,10 +138,10 @@ def _get_loaders(self):
                 loaders.append(loader)
         return loaders
 
-    def _get_paths(self) -> set[str]:
+    def get_paths(self) -> set[str]:
         """Obtains a set of all template directories."""
         paths: set[str] = set()
-        for loader in self._get_loaders():
+        for loader in self.get_loaders():
             with contextlib.suppress(ImportError, AttributeError, TypeError):
                 module = import_module(loader.__module__)
                 get_template_sources = getattr(module, "get_template_sources", None)
@@ -138,7 +150,7 @@ def _get_paths(self) -> set[str]:
                 paths.update(smart_str(origin) for origin in get_template_sources(""))
         return paths
 
-    def _get_templates(self, paths: set[str]) -> set[str]:
+    def get_templates(self, paths: set[str]) -> set[str]:
         """Obtains a set of all HTML template paths."""
         extensions = [".html"]
         templates: set[str] = set()
@@ -153,7 +165,7 @@ def _get_templates(self, paths: set[str]) -> set[str]:
 
         return templates
 
-    def _get_components(self, templates: set[str]) -> set[str]:
+    def get_components(self, templates: set[str]) -> set[str]:
         """Obtains a set of all IDOM components by parsing HTML templates."""
         components: set[str] = set()
         for template in templates:
@@ -177,7 +189,7 @@ def _get_components(self, templates: set[str]) -> set[str]:
             )
         return components
 
-    def _register_components(self, components: set[str]) -> None:
+    def register_components(self, components: set[str]) -> None:
         """Registers all IDOM components in an iterable."""
         for component in components:
             try:
@@ -194,7 +206,7 @@ def _register_components(self, components: set[str]) -> None:
                 )
 
 
-def _generate_obj_name(object: Any) -> str | None:
+def generate_obj_name(object: Any) -> str | None:
     """Makes a best effort to create a name for an object.
     Useful for JSON serialization of Python objects."""
     if hasattr(object, "__module__"):
@@ -210,7 +222,18 @@ def django_query_postprocessor(
 ) -> QuerySet | Model:
     """Recursively fetch all fields within a `Model` or `QuerySet` to ensure they are not performed lazily.
 
-    Some behaviors can be modified through `query_options` attributes."""
+    Behaviors can be modified through `QueryOptions` within your `use_query` hook.
+
+    Args:
+        data: The `Model` or `QuerySet` to recursively fetch fields from.
+
+    Keyword Args:
+        many_to_many: Whether or not to recursively fetch `ManyToManyField` relationships.
+        many_to_one: Whether or not to recursively fetch `ForeignKey` relationships.
+
+    Returns:
+        The `Model` or `QuerySet` with all fields fetched.
+    """
 
     # `QuerySet`, which is an iterable of `Model`/`QuerySet` instances
     # https://github.com/typeddjango/django-stubs/issues/704
@@ -257,3 +280,59 @@ def django_query_postprocessor(
         )
 
     return data
+
+
+def func_has_params(func: Callable, *args, **kwargs) -> bool:
+    """Checks if a function has any args or kwarg parameters.
+
+    Can optionally validate whether a set of args/kwargs would work on the given function.
+    """
+    signature = inspect.signature(func)
+
+    # Check if the function has any args/kwargs
+    if not args and not kwargs:
+        return str(signature) != "()"
+
+    # Check if the function has the given args/kwargs
+    signature.bind(*args, **kwargs)
+    return True
+
+
+def create_cache_key(*args):
+    """Creates a cache key string that starts with `django_idom` contains
+    all *args separated by `:`."""
+
+    if not args:
+        raise ValueError("At least one argument is required to create a cache key.")
+
+    return f"django_idom:{':'.join(str(arg) for arg in 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
+
+    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: datetime = timezone.make_aware(
+        datetime.strptime(cleaned_at_str or now_str, DATE_FORMAT)
+    )
+    clean_needed_by = cleaned_at + timedelta(seconds=IDOM_RECONNECT_MAX)
+    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():
+        _logger.warning(
+            "IDOM has detected component sessions in the database, "
+            "but no timestamp was found in cache. This may indicate that "
+            "the cache has been cleared."
+        )
+
+    # 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)
diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py
index c4c78971..fa90ca48 100644
--- a/src/django_idom/websocket/consumer.py
+++ b/src/django_idom/websocket/consumer.py
@@ -1,19 +1,23 @@
 """Anything used to construct a websocket endpoint"""
+from __future__ import annotations
+
 import asyncio
-import json
 import logging
-from typing import Any
-from urllib.parse import parse_qsl
+from datetime import timedelta
+from typing import Any, MutableMapping, Sequence
 
+import dill as pickle
 from channels.auth import login
 from channels.db import database_sync_to_async as convert_to_async
 from channels.generic.websocket import AsyncJsonWebsocketConsumer
+from django.utils import timezone
+from idom.backend.hooks import ConnectionContext
+from idom.backend.types import Connection, Location
 from idom.core.layout import Layout, LayoutEvent
 from idom.core.serve import serve_json_patch
 
-from django_idom.config import IDOM_REGISTERED_COMPONENTS
-from django_idom.hooks import WebsocketContext
-from django_idom.types import IdomWebsocket
+from django_idom.types import ComponentParamData, ComponentWebsocket
+from django_idom.utils import db_cleanup, func_has_params
 
 
 _logger = logging.getLogger(__name__)
@@ -23,9 +27,11 @@ class IdomAsyncWebsocketConsumer(AsyncJsonWebsocketConsumer):
     """Communicates with the browser to perform actions on-demand."""
 
     async def connect(self) -> None:
+        from django.contrib.auth.models import AbstractBaseUser
+
         await super().connect()
 
-        user = self.scope.get("user")
+        user: AbstractBaseUser = self.scope.get("user")
         if user and user.is_authenticated:
             try:
                 await login(self.scope, user)
@@ -48,22 +54,64 @@ async def receive_json(self, content: Any, **kwargs: Any) -> None:
         await self._idom_recv_queue.put(LayoutEvent(**content))
 
     async def _run_dispatch_loop(self):
-        view_id = self.scope["url_route"]["kwargs"]["view_id"]
-
+        from django_idom import models
+        from django_idom.config import IDOM_RECONNECT_MAX, IDOM_REGISTERED_COMPONENTS
+
+        scope = self.scope
+        dotted_path = scope["url_route"]["kwargs"]["dotted_path"]
+        uuid = scope["url_route"]["kwargs"]["uuid"]
+        search = scope["query_string"].decode()
+        self._idom_recv_queue: asyncio.Queue = asyncio.Queue()
+        connection = Connection(  # For `use_connection`
+            scope=scope,
+            location=Location(
+                pathname=scope["path"],
+                search=f"?{search}" if (search and (search != "undefined")) else "",
+            ),
+            carrier=ComponentWebsocket(self.close, self.disconnect, dotted_path),
+        )
+        now = timezone.now()
+        component_args: Sequence[Any] = tuple()
+        component_kwargs: MutableMapping[str, Any] = {}
+
+        # Verify the component has already been registered
         try:
-            component_constructor = IDOM_REGISTERED_COMPONENTS[view_id]
+            component_constructor = IDOM_REGISTERED_COMPONENTS[dotted_path]
         except KeyError:
-            _logger.warning(f"Unknown IDOM view ID {view_id!r}")
+            _logger.warning(
+                f"Attempt to access invalid IDOM component: {dotted_path!r}"
+            )
             return
 
-        query_dict = dict(parse_qsl(self.scope["query_string"].decode()))
-        component_kwargs = json.loads(query_dict.get("kwargs", "{}"))
-
-        # Provide developer access to parts of this websocket
-        socket = IdomWebsocket(self.scope, self.close, self.disconnect, view_id)
-
+        # Fetch the component's args/kwargs from the database, if needed
         try:
-            component_instance = component_constructor(**component_kwargs)
+            if func_has_params(component_constructor):
+                try:
+                    # Always clean up expired entries first
+                    await convert_to_async(db_cleanup)()
+
+                    # Get the queries from a DB
+                    params_query = await models.ComponentParams.objects.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:
+                    _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_args = component_params.args
+                component_kwargs = component_params.kwargs
+
+            # Generate the initial component instance
+            component_instance = component_constructor(
+                *component_args, **component_kwargs
+            )
         except Exception:
             _logger.exception(
                 f"Failed to construct component {component_constructor} "
@@ -71,12 +119,12 @@ async def _run_dispatch_loop(self):
             )
             return
 
-        self._idom_recv_queue = recv_queue = asyncio.Queue()  # type: ignore
+        # Begin serving the IDOM component
         try:
             await serve_json_patch(
-                Layout(WebsocketContext(component_instance, value=socket)),
+                Layout(ConnectionContext(component_instance, value=connection)),
                 self.send_json,
-                recv_queue.get,
+                self._idom_recv_queue.get,
             )
         except Exception:
             await self.close()
diff --git a/src/django_idom/websocket/paths.py b/src/django_idom/websocket/paths.py
index f337c83e..fab68aee 100644
--- a/src/django_idom/websocket/paths.py
+++ b/src/django_idom/websocket/paths.py
@@ -6,7 +6,7 @@
 
 
 IDOM_WEBSOCKET_PATH = path(
-    f"{IDOM_WEBSOCKET_URL}<view_id>/", IdomAsyncWebsocketConsumer.as_asgi()
+    f"{IDOM_WEBSOCKET_URL}<dotted_path>/<uuid>/", IdomAsyncWebsocketConsumer.as_asgi()
 )
 
 """A URL path for :class:`IdomAsyncWebsocketConsumer`.
diff --git a/src/js/package-lock.json b/src/js/package-lock.json
index a11a055a..fc51d27a 100644
--- a/src/js/package-lock.json
+++ b/src/js/package-lock.json
@@ -1,552 +1,552 @@
 {
-  "name": "js",
-  "lockfileVersion": 2,
-  "requires": true,
-  "packages": {
-    "": {
-      "dependencies": {
-        "idom-client-react": "^0.40.2",
-        "react": "^17.0.2",
-        "react-dom": "^17.0.2"
-      },
-      "devDependencies": {
-        "prettier": "^2.2.1",
-        "rollup": "^2.56.3",
-        "rollup-plugin-commonjs": "^10.1.0",
-        "rollup-plugin-node-resolve": "^5.2.0",
-        "rollup-plugin-replace": "^2.2.0"
-      }
-    },
-    "node_modules/@types/estree": {
-      "version": "0.0.48",
-      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz",
-      "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==",
-      "dev": true
-    },
-    "node_modules/@types/node": {
-      "version": "15.12.2",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz",
-      "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==",
-      "dev": true
-    },
-    "node_modules/@types/resolve": {
-      "version": "0.0.8",
-      "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
-      "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==",
-      "dev": true,
-      "dependencies": {
-        "@types/node": "*"
-      }
-    },
-    "node_modules/builtin-modules": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
-      "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/estree-walker": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
-      "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
-      "dev": true
-    },
-    "node_modules/fast-json-patch": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.0.tgz",
-      "integrity": "sha512-IhpytlsVTRndz0hU5t0/MGzS/etxLlfrpG5V5M9mVbuj9TrJLWaMfsox9REM5rkuGX0T+5qjpe8XA1o0gZ42nA=="
-    },
-    "node_modules/fsevents": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
-      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
-      "dev": true,
-      "hasInstallScript": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
-      }
-    },
-    "node_modules/function-bind": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
-      "dev": true
-    },
-    "node_modules/has": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
-      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
-      "dev": true,
-      "dependencies": {
-        "function-bind": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 0.4.0"
-      }
-    },
-    "node_modules/htm": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.0.tgz",
-      "integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q=="
-    },
-    "node_modules/idom-client-react": {
-      "version": "0.40.2",
-      "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.40.2.tgz",
-      "integrity": "sha512-7oTdN23DU5oeBfCjGjVovMF8vQMQiD1+89EkNgYmxJL/zQtz7HpY11fxARTIZXnB8XPvICuGEZwcPYsXkZGBFQ==",
-      "dependencies": {
-        "fast-json-patch": "^3.0.0-1",
-        "htm": "^3.0.3"
-      },
-      "peerDependencies": {
-        "react": ">=16",
-        "react-dom": ">=16"
-      }
-    },
-    "node_modules/is-core-module": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz",
-      "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
-      "dev": true,
-      "dependencies": {
-        "has": "^1.0.3"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/is-module": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
-      "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
-      "dev": true
-    },
-    "node_modules/is-reference": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
-      "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
-      "dev": true,
-      "dependencies": {
-        "@types/estree": "*"
-      }
-    },
-    "node_modules/js-tokens": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
-      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
-    },
-    "node_modules/loose-envify": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
-      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
-      "dependencies": {
-        "js-tokens": "^3.0.0 || ^4.0.0"
-      },
-      "bin": {
-        "loose-envify": "cli.js"
-      }
-    },
-    "node_modules/magic-string": {
-      "version": "0.25.7",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
-      "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
-      "dev": true,
-      "dependencies": {
-        "sourcemap-codec": "^1.4.4"
-      }
-    },
-    "node_modules/object-assign": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
-      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/path-parse": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
-      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
-      "dev": true
-    },
-    "node_modules/prettier": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz",
-      "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==",
-      "dev": true,
-      "bin": {
-        "prettier": "bin-prettier.js"
-      },
-      "engines": {
-        "node": ">=10.13.0"
-      }
-    },
-    "node_modules/react": {
-      "version": "17.0.2",
-      "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
-      "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
-      "dependencies": {
-        "loose-envify": "^1.1.0",
-        "object-assign": "^4.1.1"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/react-dom": {
-      "version": "17.0.2",
-      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
-      "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
-      "dependencies": {
-        "loose-envify": "^1.1.0",
-        "object-assign": "^4.1.1",
-        "scheduler": "^0.20.2"
-      },
-      "peerDependencies": {
-        "react": "17.0.2"
-      }
-    },
-    "node_modules/resolve": {
-      "version": "1.20.0",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
-      "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
-      "dev": true,
-      "dependencies": {
-        "is-core-module": "^2.2.0",
-        "path-parse": "^1.0.6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/rollup": {
-      "version": "2.56.3",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.56.3.tgz",
-      "integrity": "sha512-Au92NuznFklgQCUcV96iXlxUbHuB1vQMaH76DHl5M11TotjOHwqk9CwcrT78+Tnv4FN9uTBxq6p4EJoYkpyekg==",
-      "dev": true,
-      "bin": {
-        "rollup": "dist/bin/rollup"
-      },
-      "engines": {
-        "node": ">=10.0.0"
-      },
-      "optionalDependencies": {
-        "fsevents": "~2.3.2"
-      }
-    },
-    "node_modules/rollup-plugin-commonjs": {
-      "version": "10.1.0",
-      "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz",
-      "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==",
-      "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-commonjs.",
-      "dev": true,
-      "dependencies": {
-        "estree-walker": "^0.6.1",
-        "is-reference": "^1.1.2",
-        "magic-string": "^0.25.2",
-        "resolve": "^1.11.0",
-        "rollup-pluginutils": "^2.8.1"
-      },
-      "peerDependencies": {
-        "rollup": ">=1.12.0"
-      }
-    },
-    "node_modules/rollup-plugin-node-resolve": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz",
-      "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==",
-      "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-node-resolve.",
-      "dev": true,
-      "dependencies": {
-        "@types/resolve": "0.0.8",
-        "builtin-modules": "^3.1.0",
-        "is-module": "^1.0.0",
-        "resolve": "^1.11.1",
-        "rollup-pluginutils": "^2.8.1"
-      },
-      "peerDependencies": {
-        "rollup": ">=1.11.0"
-      }
-    },
-    "node_modules/rollup-plugin-replace": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz",
-      "integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==",
-      "deprecated": "This module has moved and is now available at @rollup/plugin-replace. Please update your dependencies. This version is no longer maintained.",
-      "dev": true,
-      "dependencies": {
-        "magic-string": "^0.25.2",
-        "rollup-pluginutils": "^2.6.0"
-      }
-    },
-    "node_modules/rollup-pluginutils": {
-      "version": "2.8.2",
-      "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
-      "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
-      "dev": true,
-      "dependencies": {
-        "estree-walker": "^0.6.1"
-      }
-    },
-    "node_modules/scheduler": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
-      "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
-      "dependencies": {
-        "loose-envify": "^1.1.0",
-        "object-assign": "^4.1.1"
-      }
-    },
-    "node_modules/sourcemap-codec": {
-      "version": "1.4.8",
-      "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
-      "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
-      "dev": true
-    }
-  },
-  "dependencies": {
-    "@types/estree": {
-      "version": "0.0.48",
-      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz",
-      "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==",
-      "dev": true
-    },
-    "@types/node": {
-      "version": "15.12.2",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz",
-      "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==",
-      "dev": true
-    },
-    "@types/resolve": {
-      "version": "0.0.8",
-      "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
-      "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==",
-      "dev": true,
-      "requires": {
-        "@types/node": "*"
-      }
-    },
-    "builtin-modules": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
-      "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==",
-      "dev": true
-    },
-    "estree-walker": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
-      "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
-      "dev": true
-    },
-    "fast-json-patch": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.0.tgz",
-      "integrity": "sha512-IhpytlsVTRndz0hU5t0/MGzS/etxLlfrpG5V5M9mVbuj9TrJLWaMfsox9REM5rkuGX0T+5qjpe8XA1o0gZ42nA=="
-    },
-    "fsevents": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
-      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
-      "dev": true,
-      "optional": true
-    },
-    "function-bind": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
-      "dev": true
-    },
-    "has": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
-      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
-      "dev": true,
-      "requires": {
-        "function-bind": "^1.1.1"
-      }
-    },
-    "htm": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.0.tgz",
-      "integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q=="
-    },
-    "idom-client-react": {
-      "version": "0.40.2",
-      "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.40.2.tgz",
-      "integrity": "sha512-7oTdN23DU5oeBfCjGjVovMF8vQMQiD1+89EkNgYmxJL/zQtz7HpY11fxARTIZXnB8XPvICuGEZwcPYsXkZGBFQ==",
-      "requires": {
-        "fast-json-patch": "^3.0.0-1",
-        "htm": "^3.0.3"
-      }
-    },
-    "is-core-module": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz",
-      "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
-      "dev": true,
-      "requires": {
-        "has": "^1.0.3"
-      }
-    },
-    "is-module": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
-      "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
-      "dev": true
-    },
-    "is-reference": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
-      "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
-      "dev": true,
-      "requires": {
-        "@types/estree": "*"
-      }
-    },
-    "js-tokens": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
-      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
-    },
-    "loose-envify": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
-      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
-      "requires": {
-        "js-tokens": "^3.0.0 || ^4.0.0"
-      }
-    },
-    "magic-string": {
-      "version": "0.25.7",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
-      "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
-      "dev": true,
-      "requires": {
-        "sourcemap-codec": "^1.4.4"
-      }
-    },
-    "object-assign": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
-      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
-    },
-    "path-parse": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
-      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
-      "dev": true
-    },
-    "prettier": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz",
-      "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==",
-      "dev": true
-    },
-    "react": {
-      "version": "17.0.2",
-      "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
-      "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
-      "requires": {
-        "loose-envify": "^1.1.0",
-        "object-assign": "^4.1.1"
-      }
-    },
-    "react-dom": {
-      "version": "17.0.2",
-      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
-      "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
-      "requires": {
-        "loose-envify": "^1.1.0",
-        "object-assign": "^4.1.1",
-        "scheduler": "^0.20.2"
-      }
-    },
-    "resolve": {
-      "version": "1.20.0",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
-      "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
-      "dev": true,
-      "requires": {
-        "is-core-module": "^2.2.0",
-        "path-parse": "^1.0.6"
-      }
-    },
-    "rollup": {
-      "version": "2.56.3",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.56.3.tgz",
-      "integrity": "sha512-Au92NuznFklgQCUcV96iXlxUbHuB1vQMaH76DHl5M11TotjOHwqk9CwcrT78+Tnv4FN9uTBxq6p4EJoYkpyekg==",
-      "dev": true,
-      "requires": {
-        "fsevents": "~2.3.2"
-      }
-    },
-    "rollup-plugin-commonjs": {
-      "version": "10.1.0",
-      "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz",
-      "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==",
-      "dev": true,
-      "requires": {
-        "estree-walker": "^0.6.1",
-        "is-reference": "^1.1.2",
-        "magic-string": "^0.25.2",
-        "resolve": "^1.11.0",
-        "rollup-pluginutils": "^2.8.1"
-      }
-    },
-    "rollup-plugin-node-resolve": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz",
-      "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==",
-      "dev": true,
-      "requires": {
-        "@types/resolve": "0.0.8",
-        "builtin-modules": "^3.1.0",
-        "is-module": "^1.0.0",
-        "resolve": "^1.11.1",
-        "rollup-pluginutils": "^2.8.1"
-      }
-    },
-    "rollup-plugin-replace": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz",
-      "integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==",
-      "dev": true,
-      "requires": {
-        "magic-string": "^0.25.2",
-        "rollup-pluginutils": "^2.6.0"
-      }
-    },
-    "rollup-pluginutils": {
-      "version": "2.8.2",
-      "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
-      "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
-      "dev": true,
-      "requires": {
-        "estree-walker": "^0.6.1"
-      }
-    },
-    "scheduler": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
-      "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
-      "requires": {
-        "loose-envify": "^1.1.0",
-        "object-assign": "^4.1.1"
-      }
-    },
-    "sourcemap-codec": {
-      "version": "1.4.8",
-      "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
-      "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
-      "dev": true
-    }
-  }
+	"name": "js",
+	"lockfileVersion": 2,
+	"requires": true,
+	"packages": {
+		"": {
+			"dependencies": {
+				"idom-client-react": "^0.43.0",
+				"react": "^17.0.2",
+				"react-dom": "^17.0.2"
+			},
+			"devDependencies": {
+				"prettier": "^2.2.1",
+				"rollup": "^2.56.3",
+				"rollup-plugin-commonjs": "^10.1.0",
+				"rollup-plugin-node-resolve": "^5.2.0",
+				"rollup-plugin-replace": "^2.2.0"
+			}
+		},
+		"node_modules/@types/estree": {
+			"version": "0.0.48",
+			"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz",
+			"integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==",
+			"dev": true
+		},
+		"node_modules/@types/node": {
+			"version": "15.12.2",
+			"resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz",
+			"integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==",
+			"dev": true
+		},
+		"node_modules/@types/resolve": {
+			"version": "0.0.8",
+			"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
+			"integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==",
+			"dev": true,
+			"dependencies": {
+				"@types/node": "*"
+			}
+		},
+		"node_modules/builtin-modules": {
+			"version": "3.2.0",
+			"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
+			"integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==",
+			"dev": true,
+			"engines": {
+				"node": ">=6"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/sindresorhus"
+			}
+		},
+		"node_modules/estree-walker": {
+			"version": "0.6.1",
+			"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
+			"integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
+			"dev": true
+		},
+		"node_modules/fast-json-patch": {
+			"version": "3.1.1",
+			"resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz",
+			"integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ=="
+		},
+		"node_modules/fsevents": {
+			"version": "2.3.2",
+			"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+			"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+			"dev": true,
+			"hasInstallScript": true,
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+			}
+		},
+		"node_modules/function-bind": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+			"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+			"dev": true
+		},
+		"node_modules/has": {
+			"version": "1.0.3",
+			"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+			"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+			"dev": true,
+			"dependencies": {
+				"function-bind": "^1.1.1"
+			},
+			"engines": {
+				"node": ">= 0.4.0"
+			}
+		},
+		"node_modules/htm": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/htm/-/htm-3.1.0.tgz",
+			"integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q=="
+		},
+		"node_modules/idom-client-react": {
+			"version": "0.43.0",
+			"resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.43.0.tgz",
+			"integrity": "sha512-SjVR7wmqsNO5ymKKOsLsQUA+cN12+KuG56xLkCDVyEnlDxUytIOzpzQ3qBsAeMbRJkT/BFURim7UeKnAgWgoLw==",
+			"dependencies": {
+				"fast-json-patch": "^3.1.1",
+				"htm": "^3.0.3"
+			},
+			"peerDependencies": {
+				"react": ">=16",
+				"react-dom": ">=16"
+			}
+		},
+		"node_modules/is-core-module": {
+			"version": "2.4.0",
+			"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz",
+			"integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
+			"dev": true,
+			"dependencies": {
+				"has": "^1.0.3"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/ljharb"
+			}
+		},
+		"node_modules/is-module": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
+			"integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
+			"dev": true
+		},
+		"node_modules/is-reference": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
+			"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
+			"dev": true,
+			"dependencies": {
+				"@types/estree": "*"
+			}
+		},
+		"node_modules/js-tokens": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+			"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+		},
+		"node_modules/loose-envify": {
+			"version": "1.4.0",
+			"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+			"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+			"dependencies": {
+				"js-tokens": "^3.0.0 || ^4.0.0"
+			},
+			"bin": {
+				"loose-envify": "cli.js"
+			}
+		},
+		"node_modules/magic-string": {
+			"version": "0.25.7",
+			"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
+			"integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
+			"dev": true,
+			"dependencies": {
+				"sourcemap-codec": "^1.4.4"
+			}
+		},
+		"node_modules/object-assign": {
+			"version": "4.1.1",
+			"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+			"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+			"engines": {
+				"node": ">=0.10.0"
+			}
+		},
+		"node_modules/path-parse": {
+			"version": "1.0.7",
+			"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+			"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+			"dev": true
+		},
+		"node_modules/prettier": {
+			"version": "2.3.1",
+			"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz",
+			"integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==",
+			"dev": true,
+			"bin": {
+				"prettier": "bin-prettier.js"
+			},
+			"engines": {
+				"node": ">=10.13.0"
+			}
+		},
+		"node_modules/react": {
+			"version": "17.0.2",
+			"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
+			"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
+			"dependencies": {
+				"loose-envify": "^1.1.0",
+				"object-assign": "^4.1.1"
+			},
+			"engines": {
+				"node": ">=0.10.0"
+			}
+		},
+		"node_modules/react-dom": {
+			"version": "17.0.2",
+			"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
+			"integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
+			"dependencies": {
+				"loose-envify": "^1.1.0",
+				"object-assign": "^4.1.1",
+				"scheduler": "^0.20.2"
+			},
+			"peerDependencies": {
+				"react": "17.0.2"
+			}
+		},
+		"node_modules/resolve": {
+			"version": "1.20.0",
+			"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
+			"integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
+			"dev": true,
+			"dependencies": {
+				"is-core-module": "^2.2.0",
+				"path-parse": "^1.0.6"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/ljharb"
+			}
+		},
+		"node_modules/rollup": {
+			"version": "2.56.3",
+			"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.56.3.tgz",
+			"integrity": "sha512-Au92NuznFklgQCUcV96iXlxUbHuB1vQMaH76DHl5M11TotjOHwqk9CwcrT78+Tnv4FN9uTBxq6p4EJoYkpyekg==",
+			"dev": true,
+			"bin": {
+				"rollup": "dist/bin/rollup"
+			},
+			"engines": {
+				"node": ">=10.0.0"
+			},
+			"optionalDependencies": {
+				"fsevents": "~2.3.2"
+			}
+		},
+		"node_modules/rollup-plugin-commonjs": {
+			"version": "10.1.0",
+			"resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz",
+			"integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==",
+			"deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-commonjs.",
+			"dev": true,
+			"dependencies": {
+				"estree-walker": "^0.6.1",
+				"is-reference": "^1.1.2",
+				"magic-string": "^0.25.2",
+				"resolve": "^1.11.0",
+				"rollup-pluginutils": "^2.8.1"
+			},
+			"peerDependencies": {
+				"rollup": ">=1.12.0"
+			}
+		},
+		"node_modules/rollup-plugin-node-resolve": {
+			"version": "5.2.0",
+			"resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz",
+			"integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==",
+			"deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-node-resolve.",
+			"dev": true,
+			"dependencies": {
+				"@types/resolve": "0.0.8",
+				"builtin-modules": "^3.1.0",
+				"is-module": "^1.0.0",
+				"resolve": "^1.11.1",
+				"rollup-pluginutils": "^2.8.1"
+			},
+			"peerDependencies": {
+				"rollup": ">=1.11.0"
+			}
+		},
+		"node_modules/rollup-plugin-replace": {
+			"version": "2.2.0",
+			"resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz",
+			"integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==",
+			"deprecated": "This module has moved and is now available at @rollup/plugin-replace. Please update your dependencies. This version is no longer maintained.",
+			"dev": true,
+			"dependencies": {
+				"magic-string": "^0.25.2",
+				"rollup-pluginutils": "^2.6.0"
+			}
+		},
+		"node_modules/rollup-pluginutils": {
+			"version": "2.8.2",
+			"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
+			"integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
+			"dev": true,
+			"dependencies": {
+				"estree-walker": "^0.6.1"
+			}
+		},
+		"node_modules/scheduler": {
+			"version": "0.20.2",
+			"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
+			"integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
+			"dependencies": {
+				"loose-envify": "^1.1.0",
+				"object-assign": "^4.1.1"
+			}
+		},
+		"node_modules/sourcemap-codec": {
+			"version": "1.4.8",
+			"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+			"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+			"dev": true
+		}
+	},
+	"dependencies": {
+		"@types/estree": {
+			"version": "0.0.48",
+			"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz",
+			"integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==",
+			"dev": true
+		},
+		"@types/node": {
+			"version": "15.12.2",
+			"resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz",
+			"integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==",
+			"dev": true
+		},
+		"@types/resolve": {
+			"version": "0.0.8",
+			"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
+			"integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==",
+			"dev": true,
+			"requires": {
+				"@types/node": "*"
+			}
+		},
+		"builtin-modules": {
+			"version": "3.2.0",
+			"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
+			"integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==",
+			"dev": true
+		},
+		"estree-walker": {
+			"version": "0.6.1",
+			"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
+			"integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
+			"dev": true
+		},
+		"fast-json-patch": {
+			"version": "3.1.1",
+			"resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz",
+			"integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ=="
+		},
+		"fsevents": {
+			"version": "2.3.2",
+			"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+			"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+			"dev": true,
+			"optional": true
+		},
+		"function-bind": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+			"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+			"dev": true
+		},
+		"has": {
+			"version": "1.0.3",
+			"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+			"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+			"dev": true,
+			"requires": {
+				"function-bind": "^1.1.1"
+			}
+		},
+		"htm": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/htm/-/htm-3.1.0.tgz",
+			"integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q=="
+		},
+		"idom-client-react": {
+			"version": "0.43.0",
+			"resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.43.0.tgz",
+			"integrity": "sha512-SjVR7wmqsNO5ymKKOsLsQUA+cN12+KuG56xLkCDVyEnlDxUytIOzpzQ3qBsAeMbRJkT/BFURim7UeKnAgWgoLw==",
+			"requires": {
+				"fast-json-patch": "^3.1.1",
+				"htm": "^3.0.3"
+			}
+		},
+		"is-core-module": {
+			"version": "2.4.0",
+			"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz",
+			"integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
+			"dev": true,
+			"requires": {
+				"has": "^1.0.3"
+			}
+		},
+		"is-module": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
+			"integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
+			"dev": true
+		},
+		"is-reference": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
+			"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
+			"dev": true,
+			"requires": {
+				"@types/estree": "*"
+			}
+		},
+		"js-tokens": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+			"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+		},
+		"loose-envify": {
+			"version": "1.4.0",
+			"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+			"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+			"requires": {
+				"js-tokens": "^3.0.0 || ^4.0.0"
+			}
+		},
+		"magic-string": {
+			"version": "0.25.7",
+			"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
+			"integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
+			"dev": true,
+			"requires": {
+				"sourcemap-codec": "^1.4.4"
+			}
+		},
+		"object-assign": {
+			"version": "4.1.1",
+			"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+			"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+		},
+		"path-parse": {
+			"version": "1.0.7",
+			"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+			"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+			"dev": true
+		},
+		"prettier": {
+			"version": "2.3.1",
+			"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz",
+			"integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==",
+			"dev": true
+		},
+		"react": {
+			"version": "17.0.2",
+			"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
+			"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
+			"requires": {
+				"loose-envify": "^1.1.0",
+				"object-assign": "^4.1.1"
+			}
+		},
+		"react-dom": {
+			"version": "17.0.2",
+			"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
+			"integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
+			"requires": {
+				"loose-envify": "^1.1.0",
+				"object-assign": "^4.1.1",
+				"scheduler": "^0.20.2"
+			}
+		},
+		"resolve": {
+			"version": "1.20.0",
+			"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
+			"integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
+			"dev": true,
+			"requires": {
+				"is-core-module": "^2.2.0",
+				"path-parse": "^1.0.6"
+			}
+		},
+		"rollup": {
+			"version": "2.56.3",
+			"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.56.3.tgz",
+			"integrity": "sha512-Au92NuznFklgQCUcV96iXlxUbHuB1vQMaH76DHl5M11TotjOHwqk9CwcrT78+Tnv4FN9uTBxq6p4EJoYkpyekg==",
+			"dev": true,
+			"requires": {
+				"fsevents": "~2.3.2"
+			}
+		},
+		"rollup-plugin-commonjs": {
+			"version": "10.1.0",
+			"resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz",
+			"integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==",
+			"dev": true,
+			"requires": {
+				"estree-walker": "^0.6.1",
+				"is-reference": "^1.1.2",
+				"magic-string": "^0.25.2",
+				"resolve": "^1.11.0",
+				"rollup-pluginutils": "^2.8.1"
+			}
+		},
+		"rollup-plugin-node-resolve": {
+			"version": "5.2.0",
+			"resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz",
+			"integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==",
+			"dev": true,
+			"requires": {
+				"@types/resolve": "0.0.8",
+				"builtin-modules": "^3.1.0",
+				"is-module": "^1.0.0",
+				"resolve": "^1.11.1",
+				"rollup-pluginutils": "^2.8.1"
+			}
+		},
+		"rollup-plugin-replace": {
+			"version": "2.2.0",
+			"resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz",
+			"integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==",
+			"dev": true,
+			"requires": {
+				"magic-string": "^0.25.2",
+				"rollup-pluginutils": "^2.6.0"
+			}
+		},
+		"rollup-pluginutils": {
+			"version": "2.8.2",
+			"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
+			"integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
+			"dev": true,
+			"requires": {
+				"estree-walker": "^0.6.1"
+			}
+		},
+		"scheduler": {
+			"version": "0.20.2",
+			"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
+			"integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
+			"requires": {
+				"loose-envify": "^1.1.0",
+				"object-assign": "^4.1.1"
+			}
+		},
+		"sourcemap-codec": {
+			"version": "1.4.8",
+			"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+			"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+			"dev": true
+		}
+	}
 }
diff --git a/src/js/package.json b/src/js/package.json
index 4d4112a2..10b43551 100644
--- a/src/js/package.json
+++ b/src/js/package.json
@@ -1,23 +1,23 @@
 {
-  "description": "test app for idom_django websocket server",
-  "main": "src/index.js",
-  "files": [
-    "src/**/*.js"
-  ],
-  "scripts": {
-    "build": "rollup --config",
-    "format": "prettier --ignore-path .gitignore --write ."
-  },
-  "devDependencies": {
-    "prettier": "^2.2.1",
-    "rollup": "^2.56.3",
-    "rollup-plugin-commonjs": "^10.1.0",
-    "rollup-plugin-node-resolve": "^5.2.0",
-    "rollup-plugin-replace": "^2.2.0"
-  },
-  "dependencies": {
-    "idom-client-react": "^0.40.2",
-    "react": "^17.0.2",
-    "react-dom": "^17.0.2"
-  }
+	"description": "test app for idom_django websocket server",
+	"main": "src/index.js",
+	"files": [
+		"src/**/*.js"
+	],
+	"scripts": {
+		"build": "rollup --config",
+		"format": "prettier --ignore-path .gitignore --write ."
+	},
+	"devDependencies": {
+		"prettier": "^2.2.1",
+		"rollup": "^2.56.3",
+		"rollup-plugin-commonjs": "^10.1.0",
+		"rollup-plugin-node-resolve": "^5.2.0",
+		"rollup-plugin-replace": "^2.2.0"
+	},
+	"dependencies": {
+		"idom-client-react": "^0.43.0",
+		"react": "^17.0.2",
+		"react-dom": "^17.0.2"
+	}
 }
diff --git a/src/js/rollup.config.js b/src/js/rollup.config.js
index 646f8b69..2443f388 100644
--- a/src/js/rollup.config.js
+++ b/src/js/rollup.config.js
@@ -2,32 +2,28 @@ import resolve from "rollup-plugin-node-resolve";
 import commonjs from "rollup-plugin-commonjs";
 import replace from "rollup-plugin-replace";
 
-const { PRODUCTION } = process.env;
-
 export default {
-  input: "src/index.js",
-  output: {
-    file: "../django_idom/static/django_idom/client.js",
-    format: "esm",
-  },
-  plugins: [
-    resolve(),
-    commonjs(),
-    replace({
-      "process.env.NODE_ENV": JSON.stringify(
-        PRODUCTION ? "production" : "development"
-      ),
-    }),
-  ],
-  onwarn: function (warning) {
-    // Skip certain warnings
+	input: "src/index.js",
+	output: {
+		file: "../django_idom/static/django_idom/client.js",
+		format: "esm",
+	},
+	plugins: [
+		resolve(),
+		commonjs(),
+		replace({
+			"process.env.NODE_ENV": JSON.stringify("production"),
+		}),
+	],
+	onwarn: function (warning) {
+		// Skip certain warnings
 
-    // should intercept ... but doesn't in some rollup versions
-    if (warning.code === "THIS_IS_UNDEFINED") {
-      return;
-    }
+		// should intercept ... but doesn't in some rollup versions
+		if (warning.code === "THIS_IS_UNDEFINED") {
+			return;
+		}
 
-    // console.warn everything else
-    console.warn(warning.message);
-  },
+		// console.warn everything else
+		console.warn(warning.message);
+	},
 };
diff --git a/src/js/src/index.js b/src/js/src/index.js
index 8f17c0d4..478bf8ca 100644
--- a/src/js/src/index.js
+++ b/src/js/src/index.js
@@ -1,37 +1,34 @@
 import { mountLayoutWithWebSocket } from "idom-client-react";
 
 // Set up a websocket at the base endpoint
-let LOCATION = window.location;
+const LOCATION = window.location;
 let WS_PROTOCOL = "";
 if (LOCATION.protocol == "https:") {
-  WS_PROTOCOL = "wss://";
+	WS_PROTOCOL = "wss://";
 } else {
-  WS_PROTOCOL = "ws://";
+	WS_PROTOCOL = "ws://";
 }
-let WS_ENDPOINT_URL = WS_PROTOCOL + LOCATION.host + "/";
+const WS_ENDPOINT_URL = WS_PROTOCOL + LOCATION.host + "/";
 
 export function mountViewToElement(
-  mountPoint,
-  idomWebsocketUrl,
-  idomWebModulesUrl,
-  maxReconnectTimeout,
-  viewId,
-  queryParams
+	mountElement,
+	idomWebsocketUrl,
+	idomWebModulesUrl,
+	maxReconnectTimeout,
+	componentPath
 ) {
-  const fullWebsocketUrl =
-    WS_ENDPOINT_URL + idomWebsocketUrl + viewId + "/?" + queryParams;
+	const WS_URL = WS_ENDPOINT_URL + idomWebsocketUrl + componentPath;
+	const WEB_MODULE_URL = LOCATION.origin + "/" + idomWebModulesUrl;
+	const loadImportSource = (source, sourceType) => {
+		return import(
+			sourceType == "NAME" ? `${WEB_MODULE_URL}${source}` : source
+		);
+	};
 
-  const fullWebModulesUrl = LOCATION.origin + "/" + idomWebModulesUrl;
-  const loadImportSource = (source, sourceType) => {
-    return import(
-      sourceType == "NAME" ? `${fullWebModulesUrl}${source}` : source
-    );
-  };
-
-  mountLayoutWithWebSocket(
-    mountPoint,
-    fullWebsocketUrl,
-    loadImportSource,
-    maxReconnectTimeout
-  );
+	mountLayoutWithWebSocket(
+		mountElement,
+		WS_URL,
+		loadImportSource,
+		maxReconnectTimeout
+	);
 }


Test patch:
diff --git a/docs/src/contribute/running-tests.md b/docs/src/contribute/running-tests.md
index 8f806e08..5d4c48cf 100644
--- a/docs/src/contribute/running-tests.md
+++ b/docs/src/contribute/running-tests.md
@@ -7,7 +7,7 @@ If you plan to run tests, you will need to install the following dependencies fi
 
 Once done, you should clone this repository:
 
-```bash
+```bash linenums="0"
 git clone https://github.com/idom-team/django-idom.git
 cd django-idom
 pip install -r ./requirements/test-run.txt --upgrade
@@ -15,12 +15,12 @@ pip install -r ./requirements/test-run.txt --upgrade
 
 Then, by running the command below you can run the full test suite:
 
-```
+```bash linenums="0"
 nox -s test
 ```
 
 Or, if you want to run the tests in the foreground:
 
-```
+```bash linenums="0"
 nox -s test -- --headed
 ```
diff --git a/tests/test_app/admin.py b/tests/test_app/admin.py
index 5e35a515..e0701a96 100644
--- a/tests/test_app/admin.py
+++ b/tests/test_app/admin.py
@@ -1,6 +1,8 @@
 from django.contrib import admin
 from test_app.models import ForiegnChild, RelationalChild, RelationalParent, TodoItem
 
+from django_idom.models import ComponentParams
+
 
 @admin.register(TodoItem)
 class TodoItemAdmin(admin.ModelAdmin):
@@ -20,3 +22,8 @@ class RelationalParentAdmin(admin.ModelAdmin):
 @admin.register(ForiegnChild)
 class ForiegnChildAdmin(admin.ModelAdmin):
     pass
+
+
+@admin.register(ComponentParams)
+class ComponentParamsAdmin(admin.ModelAdmin):
+    list_display = ("uuid", "last_accessed")
diff --git a/tests/test_app/components.py b/tests/test_app/components.py
index 37ae1cda..38ba30a5 100644
--- a/tests/test_app/components.py
+++ b/tests/test_app/components.py
@@ -8,14 +8,14 @@
 
 import django_idom
 from django_idom.components import view_to_component
-from django_idom.hooks import use_mutation, use_query
 
 from . import views
+from .types import TestObject
 
 
 @component
 def hello_world():
-    return html._(html.h1({"id": "hello-world"}, "Hello World!"), html.hr())
+    return html._(html.div({"id": "hello-world"}, "Hello World!"), html.hr())
 
 
 @component
@@ -23,6 +23,7 @@ def button():
     count, set_count = hooks.use_state(0)
     return html._(
         html.div(
+            "button:",
             html.button(
                 {"id": "counter-inc", "onClick": lambda event: set_count(count + 1)},
                 "Click me!",
@@ -40,7 +41,27 @@ def button():
 def parameterized_component(x, y):
     total = x + y
     return html._(
-        html.h1({"id": "parametrized-component", "data-value": total}, total),
+        html.div(
+            {"id": "parametrized-component", "data-value": total},
+            f"parameterized_component: {total}",
+        ),
+        html.hr(),
+    )
+
+
+@component
+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(
+            {
+                "id": co_name,
+                "data-success": success,
+            },
+            f"{co_name}: ",
+            str(my_object),
+        ),
         html.hr(),
     )
 
@@ -57,18 +78,25 @@ def parameterized_component(x, y):
 @component
 def simple_button():
     return html._(
+        "simple_button:",
         SimpleButton({"id": "simple-button"}),
         html.hr(),
     )
 
 
 @component
-def use_websocket():
-    ws = django_idom.hooks.use_websocket()
-    success = bool(ws.scope and ws.close and ws.disconnect and ws.view_id)
+def use_connection():
+    ws = django_idom.hooks.use_connection()
+    success = bool(
+        ws.scope
+        and getattr(ws, "location", None)
+        and getattr(ws.carrier, "close", None)
+        and getattr(ws.carrier, "disconnect", None)
+        and getattr(ws.carrier, "dotted_path", None)
+    )
     return html.div(
-        {"id": "use-websocket", "data-success": success},
-        f"use_websocket: {ws}",
+        {"id": "use-connection", "data-success": success},
+        f"use_connection: {ws}",
         html.hr(),
     )
 
@@ -191,8 +219,8 @@ def get_foriegn_child_query():
 
 @component
 def relational_query():
-    foriegn_child = use_query(get_foriegn_child_query)
-    relational_parent = use_query(get_relational_parent_query)
+    foriegn_child = django_idom.hooks.use_query(get_foriegn_child_query)
+    relational_parent = django_idom.hooks.use_query(get_relational_parent_query)
 
     if not relational_parent.data or not foriegn_child.data:
         return
@@ -239,8 +267,8 @@ def toggle_todo_mutation(item: TodoItem):
 @component
 def todo_list():
     input_value, set_input_value = hooks.use_state("")
-    items = use_query(get_todo_query)
-    toggle_item = use_mutation(toggle_todo_mutation)
+    items = django_idom.hooks.use_query(get_todo_query)
+    toggle_item = django_idom.hooks.use_mutation(toggle_todo_mutation)
 
     if items.error:
         rendered_items = html.h2(f"Error when loading - {items.error}")
@@ -254,7 +282,7 @@ def todo_list():
             _render_todo_items([i for i in items.data if i.done], toggle_item),
         )
 
-    add_item = use_mutation(add_todo_mutation, refetch=get_todo_query)
+    add_item = django_idom.hooks.use_mutation(add_todo_mutation, refetch=get_todo_query)
 
     if add_item.loading:
         mutation_status = html.h2("Working...")
diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py
index 6cb4dc82..bbc4be6a 100644
--- a/tests/test_app/settings.py
+++ b/tests/test_app/settings.py
@@ -80,6 +80,14 @@
     },
 }
 
+# Cache
+CACHES = {
+    "default": {
+        "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
+        "LOCATION": os.path.join(BASE_DIR, "cache"),
+    }
+}
+
 # Password validation
 # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
 AUTH_PASSWORD_VALIDATORS = [
diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html
index ca196297..8b7344b2 100644
--- a/tests/test_app/templates/base.html
+++ b/tests/test_app/templates/base.html
@@ -23,8 +23,9 @@ <h1>IDOM Test Page</h1>
   <div>{% component "test_app.components.hello_world" class="hello-world" %}</div>
   <div>{% component "test_app.components.button" class="button" %}</div>
   <div>{% component "test_app.components.parameterized_component" class="parametarized-component" x=123 y=456 %}</div>
+  <div>{% component "test_app.components.object_in_templatetag" my_object %}</div>
   <div>{% component "test_app.components.simple_button" %}</div>
-  <div>{% component "test_app.components.use_websocket" %}</div>
+  <div>{% component "test_app.components.use_connection" %}</div>
   <div>{% component "test_app.components.use_scope" %}</div>
   <div>{% component "test_app.components.use_location" %}</div>
   <div>{% component "test_app.components.use_origin" %}</div>
diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py
index e5a77827..359b7dbe 100644
--- a/tests/test_app/tests/test_components.py
+++ b/tests/test_app/tests/test_components.py
@@ -9,7 +9,7 @@
 CLICK_DELAY = 250  # Delay in miliseconds. Needed for GitHub Actions.
 
 
-class TestIdomCapabilities(ChannelsLiveServerTestCase):
+class ComponentTests(ChannelsLiveServerTestCase):
     @classmethod
     def setUpClass(cls):
         if sys.platform == "win32":
@@ -47,11 +47,14 @@ def test_counter(self):
     def test_parametrized_component(self):
         self.page.locator("#parametrized-component[data-value='579']").wait_for()
 
+    def test_object_in_templatetag(self):
+        self.page.locator("#object_in_templatetag[data-success=true]").wait_for()
+
     def test_component_from_web_module(self):
         self.page.wait_for_selector("#simple-button")
 
-    def test_use_websocket(self):
-        self.page.locator("#use-websocket[data-success=true]").wait_for()
+    def test_use_connection(self):
+        self.page.locator("#use-connection[data-success=true]").wait_for()
 
     def test_use_scope(self):
         self.page.locator("#use-scope[data-success=true]").wait_for()
diff --git a/tests/test_app/tests/test_database.py b/tests/test_app/tests/test_database.py
new file mode 100644
index 00000000..3709de08
--- /dev/null
+++ b/tests/test_app/tests/test_database.py
@@ -0,0 +1,46 @@
+from time import sleep
+from typing import Any
+from uuid import uuid4
+
+import dill as pickle
+from django.test import TransactionTestCase
+
+from django_idom import utils
+from django_idom.models import ComponentParams
+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)
+        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
+
+        # Force `params_1` to expire
+        from django_idom import config
+
+        config.IDOM_RECONNECT_MAX = 1
+        sleep(config.IDOM_RECONNECT_MAX + 0.1)
+
+        # Create a new, non-expired component params
+        params_2 = self._save_params_to_db(2)
+        self.assertEqual(ComponentParams.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
+
+    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.full_clean()
+        model.save()
+
+        return param_data
diff --git a/tests/test_app/types.py b/tests/test_app/types.py
new file mode 100644
index 00000000..438c69d0
--- /dev/null
+++ b/tests/test_app/types.py
@@ -0,0 +1,6 @@
+from dataclasses import dataclass
+
+
+@dataclass
+class TestObject:
+    value: int = 0
diff --git a/tests/test_app/views.py b/tests/test_app/views.py
index 2d16ae97..b172132a 100644
--- a/tests/test_app/views.py
+++ b/tests/test_app/views.py
@@ -4,9 +4,11 @@
 from django.shortcuts import render
 from django.views.generic import TemplateView, View
 
+from .types import TestObject
+
 
 def base_template(request):
-    return render(request, "base.html", {})
+    return render(request, "base.html", {"my_object": TestObject(1)})
 
 
 def view_to_component_sync_func(request):