Repository: xpodev/seamless (language: python)

Issue:
Allow router to pass props for rendered component
I'm trying to build a small chat application in `seamless`.

I have a login page that asks the user to enter their name. Then, I want the client to send an event
to the server and also render the chat component.

However, I also need the name the user entered in order to render the chat component.

So, I thought maybe I can have the chat component on a separate `Route` but I can't pass the user
name to the chat component.

What I do now is to have custom JS code that detects when the user presses the submit button on
the login page and manually render the chat component.

```py
# login.py

from pydantic import BaseModel
from seamless import Component, Div, Input, Button, Span, JS, Form
from seamless.styling import StyleObject
from events import SubmitEvent

from chat_server import SERVER


class Login(Component):
    class LoginForm(BaseModel):
        name: str

    def render(self):
        return Div(
            Div(
                style=StyleObject(
                    height="30px",
                    background="lightgray",
                    width="100%",
                )
            )(
                Span("Login")
            ),
            Div()(
                Form(id="loginForm", on_submit=self.connect)(
                    Input(name="name", init=JS("this.parentElement.addEventListener('submit', async () => {seamless.instance.render(await seamless.instance.getComponent('ChatRoom', {name: this.value}), document.getElementById('content'))});")),
                    Button(type="submit"
                    )(
                        "Login"
                    )
                )
            )
        )
    
    async def connect(self, data: SubmitEvent[LoginForm]):
        await SERVER.join(data.data.name)
```

As you can see, the JS code is really long and ugly so I want to get rid of it.


Discussion / hints:

Fix patch:
diff --git a/.vscode/launch.json b/.vscode/launch.json
index b37e22e..7f7f91d 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -4,7 +4,6 @@
       "name": "Python Debugger: Test File - WSGI",
       "type": "debugpy",
       "request": "launch",
-      // "python": "C:\\Users\\ee102\\Documents\\ez-web\\lib\\python\\python.exe",
       "program": "${workspaceFolder}/test.py",
       "args": [
         "--type",
@@ -17,7 +16,6 @@
       "name": "Python Debugger: Test File - ASGI",
       "type": "debugpy",
       "request": "launch",
-      // "python": "C:\\Users\\ee102\\Documents\\ez-web\\lib\\python\\python.exe",
       "program": "${workspaceFolder}/test.py",
       "args": [
         "--type",
@@ -30,7 +28,6 @@
       "name": "Python Debugger: Test File - Manual",
       "type": "debugpy",
       "request": "launch",
-      // "python": "C:\\Users\\ee102\\Documents\\ez-web\\lib\\python\\python.exe",
       "program": "${workspaceFolder}/test.py",
       "args": [
         "--type",
@@ -43,13 +40,23 @@
       "name": "Python Debugger: Example",
       "type": "debugpy",
       "request": "launch",
-      // "python": "C:\\Users\\ee102\\Documents\\ez-web\\lib\\python\\python.exe",
       "program": "${workspaceFolder}/examples/simple/main.py",
       "env": {
         "PYTHONPATH": "${workspaceFolder}"
       },
       "console": "integratedTerminal",
       "justMyCode": false
+    },
+    {
+      "name": "Python Debugger: SPA",
+      "type": "debugpy",
+      "request": "launch",
+      "program": "${workspaceFolder}/examples/spa/main.py",
+      "env": {
+        "PYTHONPATH": "${workspaceFolder}"
+      },
+      "console": "integratedTerminal",
+      "justMyCode": false
     }
   ]
 }
diff --git a/README.md b/README.md
index 40e16ae..60213b2 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
 
 Seamless is a Python package for creating and manipulating HTML components. It is working similar to React.js, but in Python.
 
-[Docs](https://seamless.readthedocs.io/)
+We have detailed documentation [here](https://seamless.readthedocs.io/)
 
 ## Installation
 ```sh
diff --git a/_auto_html.py b/_auto_html.py
index 78781b5..f3626fb 100644
--- a/_auto_html.py
+++ b/_auto_html.py
@@ -49,7 +49,7 @@
     "Heading": "HTMLHeadingElement",
     "Html": "HTMLHtmlElement",
     "IFrame": "HTMLIFrameElement",
-    "Image": "HTMLImageElement",
+    "Img": "HTMLImageElement",
     "Input": "HTMLInputElement",
     "Ins": "HTMLModElement",
     "Li": "HTMLListItemElement",
diff --git a/docs/conf.py b/docs/conf.py
index fdf76c6..8be564d 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,3 +1,5 @@
+from seamless import __version__
+
 # Configuration file for the Sphinx documentation builder.
 #
 # For the full list of built-in configuration values, see the documentation:
@@ -9,7 +11,7 @@
 project = 'Seamless'
 copyright = '2024, Xpo Development'
 author = 'Xpo Development'
-version = '0.8.6'
+version = __version__
 
 # -- General configuration ---------------------------------------------------
 # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
diff --git a/examples/simple/components/app.py b/examples/simple/components/app.py
index 84a45df..80f254b 100644
--- a/examples/simple/components/app.py
+++ b/examples/simple/components/app.py
@@ -1,17 +1,25 @@
-from seamless import Component, Div, Nav
-from seamless.extra import State
+from seamless import Component, Div, Nav, Button
+from seamless.extensions import State
 from seamless.components.router import Router, Route, RouterLink
 from pages import HomePage, CounterPage, BasePage
 from components.loading import Loading
 
+
 class App(Component):
     def render(self):
         return BasePage(
             State.init(),
             Div(class_name="d-flex flex-column h-100")(
-                Nav(class_name="navbar navbar-expand-lg navbar-light bg-light")(
-                    RouterLink(to="/", class_name="navbar-brand")("Home"),
-                    RouterLink(to="/counter", class_name="navbar-brand")("Counter"),
+                Div(class_name="d-flex justify-content-between")(
+                    Nav(class_name="navbar navbar-expand-lg navbar-light bg-light")(
+                        RouterLink(to="/", class_name="navbar-brand")("Home"),
+                        RouterLink(to="/counter", class_name="navbar-brand")("Counter"),
+                    ),
+                    Div(
+                        Button(on_click=self.foo)(
+                            "Click me!"
+                        )
+                    )
                 ),
                 Div(class_name="content flex-grow-1")(
                     Router(loading_component=Loading)(
@@ -22,3 +30,6 @@ def render(self):
             ),
             title="Seamless",
         )
+
+    def foo(self, event):
+        print("foo")
\ No newline at end of file
diff --git a/examples/simple/pages/base.py b/examples/simple/pages/base.py
index dec32f5..e2a858c 100644
--- a/examples/simple/pages/base.py
+++ b/examples/simple/pages/base.py
@@ -1,4 +1,4 @@
-from seamless import Link, Script, Style
+from seamless import Link, Script, Style, __version__
 from seamless.components import Page
 from seamless.styling import CSS
 
@@ -9,7 +9,7 @@ def head(self):
             rel="stylesheet",
             href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css",
         )
-        yield Script(src="https://cdn.jsdelivr.net/npm/@python-seamless/core@0.8.6/umd/seamless.init.js", defer=True)
+        yield Script(src=f"https://cdn.jsdelivr.net/npm/@python-seamless/core@{__version__}/umd/seamless.init.js", defer=True)
         yield Style(
             "html, body { height: 100%; }" +
             CSS.to_css_string(minified=True)
diff --git a/examples/simple/pages/counter.py b/examples/simple/pages/counter.py
index 77cc3b2..13eeeb0 100644
--- a/examples/simple/pages/counter.py
+++ b/examples/simple/pages/counter.py
@@ -1,7 +1,7 @@
 from pydantic import BaseModel
 from seamless import Div, Form, Input, Button, Component
 from seamless.core import SocketID
-from seamless.extra import State
+from seamless.extensions import State
 from seamless.types.events import SubmitEvent
 
 
diff --git a/examples/simple/pages/home.py b/examples/simple/pages/home.py
index 2a4ab5f..1a65e99 100644
--- a/examples/simple/pages/home.py
+++ b/examples/simple/pages/home.py
@@ -1,5 +1,5 @@
 from seamless import Div, Component
-from seamless.extra import State
+from seamless.extensions import State
 from components.clock import Clock
 
 
diff --git a/examples/spa/components/app.py b/examples/spa/components/app.py
new file mode 100644
index 0000000..201ba48
--- /dev/null
+++ b/examples/spa/components/app.py
@@ -0,0 +1,36 @@
+from seamless import Component, Div, Nav, Button
+from seamless.extensions import State
+from seamless.components.router import Router, Route, RouterLink
+from pages import HomePage, CounterPage, BasePage, UserPage
+from components.loading import Loading
+
+
+class App(Component):
+    def render(self):
+        return BasePage(
+            State.init(),
+            Div(class_name="d-flex flex-column h-100")(
+                Div(class_name="d-flex justify-content-between")(
+                    Nav(class_name="navbar navbar-expand-lg navbar-light bg-light")(
+                        RouterLink(to="/", class_name="navbar-brand")("Home"),
+                        RouterLink(to="/counter", class_name="navbar-brand")("Counter"),
+                    ),
+                    Div(
+                        Button(on_click=self.foo)(
+                            "Click me!"
+                        )
+                    )
+                ),
+                Div(class_name="content flex-grow-1")(
+                    Router(loading_component=Loading)(
+                        Route(path="/", component=HomePage),
+                        Route(path="/counter", component=CounterPage),
+                        Route(path="/user/{user_id:int}", component=UserPage),
+                    )
+                ),
+            ),
+            title="Seamless",
+        )
+
+    def foo(self, event):
+        print("foo")
\ No newline at end of file
diff --git a/examples/spa/components/loading.css b/examples/spa/components/loading.css
new file mode 100644
index 0000000..192dc7a
--- /dev/null
+++ b/examples/spa/components/loading.css
@@ -0,0 +1,15 @@
+.spinner {
+  display: inline-block;
+  width: 50px;
+  height: 50px;
+  border: 3px solid rgba(0, 0, 0, 0.3);
+  border-radius: 50%;
+  border-top-color: #333;
+  animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+  to {
+    transform: rotate(360deg);
+  }
+}
\ No newline at end of file
diff --git a/examples/spa/components/loading.py b/examples/spa/components/loading.py
new file mode 100644
index 0000000..321e0c8
--- /dev/null
+++ b/examples/spa/components/loading.py
@@ -0,0 +1,13 @@
+from seamless import Component, Div
+from seamless.styling import CSS
+
+styles = CSS.module("./loading.css")
+
+
+class Loading(Component):
+    def render(self):
+        return Div(class_name="d-flex align-items-center justify-content-center h-100")(
+            Div(
+                class_name=f"{styles.spinner}",
+            ),
+        )
diff --git a/examples/spa/main.py b/examples/spa/main.py
new file mode 100644
index 0000000..8aa1094
--- /dev/null
+++ b/examples/spa/main.py
@@ -0,0 +1,28 @@
+from pathlib import Path
+from fastapi import FastAPI
+from fastapi.responses import HTMLResponse, FileResponse
+from seamless import render
+from seamless.middlewares import ASGIMiddleware
+
+from components.app import App
+
+HERE = Path(__file__).parent
+
+app = FastAPI()
+app.add_middleware(ASGIMiddleware)
+
+
+@app.get("/static/{file_path:path}")
+def read_static(file_path: str):
+    return FileResponse(HERE / "static" / file_path)
+
+
+@app.get("/{full_path:path}", response_class=HTMLResponse)
+def read_root():
+    return render(App())
+
+
+if __name__ == "__main__":
+    import uvicorn
+
+    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
diff --git a/examples/spa/pages/__init__.py b/examples/spa/pages/__init__.py
new file mode 100644
index 0000000..09a0ce1
--- /dev/null
+++ b/examples/spa/pages/__init__.py
@@ -0,0 +1,6 @@
+from .base import BasePage
+from .counter import CounterPage
+from .home import HomePage
+from .user import UserPage
+
+__all__ = ["BasePage", "CounterPage", "HomePage", "UserPage"]
diff --git a/examples/spa/pages/base.py b/examples/spa/pages/base.py
new file mode 100644
index 0000000..e2a858c
--- /dev/null
+++ b/examples/spa/pages/base.py
@@ -0,0 +1,16 @@
+from seamless import Link, Script, Style, __version__
+from seamless.components import Page
+from seamless.styling import CSS
+
+class BasePage(Page):
+    def head(self):
+        yield from super().head()
+        yield Link(
+            rel="stylesheet",
+            href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css",
+        )
+        yield Script(src=f"https://cdn.jsdelivr.net/npm/@python-seamless/core@{__version__}/umd/seamless.init.js", defer=True)
+        yield Style(
+            "html, body { height: 100%; }" +
+            CSS.to_css_string(minified=True)
+        )
diff --git a/examples/spa/pages/counter.py b/examples/spa/pages/counter.py
new file mode 100644
index 0000000..13eeeb0
--- /dev/null
+++ b/examples/spa/pages/counter.py
@@ -0,0 +1,53 @@
+from pydantic import BaseModel
+from seamless import Div, Form, Input, Button, Component
+from seamless.core import SocketID
+from seamless.extensions import State
+from seamless.types.events import SubmitEvent
+
+
+class Counter(BaseModel):
+    counter_value: int
+
+
+counter = State("counter", 0)
+
+
+class CounterPage(Component):
+    def render(self):
+        return Div(class_name="container")(
+            Div(class_name="row")(
+                Div(class_name="display-1 text-center")("Counter"),
+                Div(class_name="display-6 text-center")("A simple counter page."),
+            ),
+            Div(class_name="row mt-4")(
+                Div(class_name="lead col-12 text-center")(
+                    "Current counter: ", counter()
+                ),
+            ),
+            Div(class_name="row")(
+                Div(class_name="col-12 text-center")(
+                    Div(class_name="btn-group")(
+                        Div(class_name="btn btn-danger", on_click=counter("state - 1"))(
+                            "Decrement"
+                        ),
+                        Div(class_name="btn btn-primary", on_click=counter("0"))(
+                            "Reset"
+                        ),
+                        Div(
+                            class_name="btn btn-success", on_click=counter("state + 1")
+                        )("Increment"),
+                    ),
+                ),
+                Div(class_name="col-12 text-center mt-4")(
+                    Form(action="#", on_submit=self.submit)(
+                        Input(type="hidden", name="counter_value", value=counter()),
+                        Button(class_name="btn btn-primary", type="submit")("Submit"),
+                    )
+                ),
+            ),
+        )
+
+    def submit(self, event: SubmitEvent[Counter], sid: SocketID):
+        print("Form submitted!")
+        print("socket id:", sid)
+        print(event)
diff --git a/examples/spa/pages/home.py b/examples/spa/pages/home.py
new file mode 100644
index 0000000..81123e0
--- /dev/null
+++ b/examples/spa/pages/home.py
@@ -0,0 +1,23 @@
+from seamless import Div, Component, Input, Button, JS
+from seamless.extensions import State
+
+search_user = State("search_user")
+
+class HomePage(Component):
+    def render(self):
+        return Div(class_name="container")(
+            Div(class_name="row")(
+                Div(class_name="display-1 text-center")("Welcome to Seamless!"),
+                Div(class_name="display-6 text-center")(
+                    "A Python library for building web pages using Python."
+                ),
+            ),
+            Div(class_name="row mt-4")(
+                Div(class_name="lead col-12 text-center")("Current counter: ", State("counter").get()),
+            ),
+            Div(class_name="row mt-4")(
+                "Search User: ",
+                Input(type="text", on_change=search_user("this.value")),
+                Button(on_click=JS(f"seamless.navigateTo('/user/' + {search_user} + '?a=1')"))("Search"),
+            ),
+        )
diff --git a/examples/spa/pages/user.py b/examples/spa/pages/user.py
new file mode 100644
index 0000000..8888c0e
--- /dev/null
+++ b/examples/spa/pages/user.py
@@ -0,0 +1,40 @@
+from pathlib import Path
+from dataclasses import dataclass
+from json import load
+from seamless import Div, Component, Img
+
+HERE = Path(__file__).parent
+
+users = load((HERE / "users.json").open(encoding="utf-8"))
+
+
+@dataclass
+class UserPage(Component):
+    user_id: int
+
+    def render(self):
+        if self.user_id >= len(users):
+            return Div(class_name="container")(
+                Div(class_name="row")(
+                    Div(class_name="display-1 text-center")("User not found"),
+                )
+            )
+
+        user = users[self.user_id]
+        user_name = (
+            f"{user['name']['title']} {user['name']['first']} {user['name']['last']}"
+        )
+
+        return Div(class_name="container")(
+            Div(class_name="row")(
+                Div(class_name="text-center")(
+                    Img(
+                        src=user["picture"]["large"],
+                        alt=user_name,
+                        class_name="rounded-circle",
+                    ),
+                ),
+                Div(class_name="display-1 text-center")(user_name),
+                Div(class_name="display-6 text-center")(f"Email: {user['email']}"),
+            ),
+        )
diff --git a/examples/spa/pages/users.json b/examples/spa/pages/users.json
new file mode 100644
index 0000000..1f5048b
--- /dev/null
+++ b/examples/spa/pages/users.json
@@ -0,0 +1,132 @@
+[
+  {
+      "name": {
+          "title": "Mr",
+          "first": "Lorenzo",
+          "last": "Leroux"
+      },
+      "email": "lorenzo.leroux@example.com",
+      "picture": {
+          "large": "https://randomuser.me/api/portraits/men/26.jpg",
+          "medium": "https://randomuser.me/api/portraits/med/men/26.jpg",
+          "thumbnail": "https://randomuser.me/api/portraits/thumb/men/26.jpg"
+      }
+  },
+  {
+      "name": {
+          "title": "Mr",
+          "first": "Alessandro",
+          "last": "Visscher"
+      },
+      "email": "alessandro.visscher@example.com",
+      "picture": {
+          "large": "https://randomuser.me/api/portraits/men/15.jpg",
+          "medium": "https://randomuser.me/api/portraits/med/men/15.jpg",
+          "thumbnail": "https://randomuser.me/api/portraits/thumb/men/15.jpg"
+      }
+  },
+  {
+      "name": {
+          "title": "Miss",
+          "first": "Nanna",
+          "last": "Nielsen"
+      },
+      "email": "nanna.nielsen@example.com",
+      "picture": {
+          "large": "https://randomuser.me/api/portraits/women/39.jpg",
+          "medium": "https://randomuser.me/api/portraits/med/women/39.jpg",
+          "thumbnail": "https://randomuser.me/api/portraits/thumb/women/39.jpg"
+      }
+  },
+  {
+      "name": {
+          "title": "Mr",
+          "first": "Emmanuel",
+          "last": "Harsvik"
+      },
+      "email": "emmanuel.harsvik@example.com",
+      "picture": {
+          "large": "https://randomuser.me/api/portraits/men/44.jpg",
+          "medium": "https://randomuser.me/api/portraits/med/men/44.jpg",
+          "thumbnail": "https://randomuser.me/api/portraits/thumb/men/44.jpg"
+      }
+  },
+  {
+      "name": {
+          "title": "Mr",
+          "first": "Teodoro",
+          "last": "Archuleta"
+      },
+      "email": "teodoro.archuleta@example.com",
+      "picture": {
+          "large": "https://randomuser.me/api/portraits/men/35.jpg",
+          "medium": "https://randomuser.me/api/portraits/med/men/35.jpg",
+          "thumbnail": "https://randomuser.me/api/portraits/thumb/men/35.jpg"
+      }
+  },
+  {
+      "name": {
+          "title": "Mr",
+          "first": "علی",
+          "last": "پارسا"
+      },
+      "email": "aaly.prs@example.com",
+      "picture": {
+          "large": "https://randomuser.me/api/portraits/men/10.jpg",
+          "medium": "https://randomuser.me/api/portraits/med/men/10.jpg",
+          "thumbnail": "https://randomuser.me/api/portraits/thumb/men/10.jpg"
+      }
+  },
+  {
+      "name": {
+          "title": "Mr",
+          "first": "Ismet",
+          "last": "Steck"
+      },
+      "email": "ismet.steck@example.com",
+      "picture": {
+          "large": "https://randomuser.me/api/portraits/men/91.jpg",
+          "medium": "https://randomuser.me/api/portraits/med/men/91.jpg",
+          "thumbnail": "https://randomuser.me/api/portraits/thumb/men/91.jpg"
+      }
+  },
+  {
+      "name": {
+          "title": "Mr",
+          "first": "Rodrigo",
+          "last": "Longoria"
+      },
+      "email": "rodrigo.longoria@example.com",
+      "picture": {
+          "large": "https://randomuser.me/api/portraits/men/61.jpg",
+          "medium": "https://randomuser.me/api/portraits/med/men/61.jpg",
+          "thumbnail": "https://randomuser.me/api/portraits/thumb/men/61.jpg"
+      }
+  },
+  {
+      "name": {
+          "title": "Miss",
+          "first": "Sue",
+          "last": "Warren"
+      },
+      "email": "sue.warren@example.com",
+      "picture": {
+          "large": "https://randomuser.me/api/portraits/women/96.jpg",
+          "medium": "https://randomuser.me/api/portraits/med/women/96.jpg",
+          "thumbnail": "https://randomuser.me/api/portraits/thumb/women/96.jpg"
+      }
+  },
+  {
+      "name": {
+          "title": "Mrs",
+          "first": "Emily",
+          "last": "Hawkins"
+      },
+      "email": "emily.hawkins@example.com",
+      "picture": {
+          "large": "https://randomuser.me/api/portraits/women/68.jpg",
+          "medium": "https://randomuser.me/api/portraits/med/women/68.jpg",
+          "thumbnail": "https://randomuser.me/api/portraits/thumb/women/68.jpg"
+      }
+  }
+]
\ No newline at end of file
diff --git a/node/dev.ps1 b/node/dev.ps1
index 5564fa6..77a0ba4 100644
--- a/node/dev.ps1
+++ b/node/dev.ps1
@@ -1,4 +1,5 @@
 yarn build:browser
 Copy-Item -Path ./packages/core/umd/seamless.min.js -Destination ../tests/server/static/main.js
 Add-Content -Path ../tests/server/static/main.js -Value "const s = new Seamless();"
-Copy-Item -Path ../tests/server/static/main.js -Destination ../examples/simple/static/main.js
\ No newline at end of file
+Copy-Item -Path ../tests/server/static/main.js -Destination ../examples/simple/static/main.js
+Copy-Item -Path ../tests/server/static/main.js -Destination ../examples/spa/static/main.js
\ No newline at end of file
diff --git a/node/packages/core/src/index.ts b/node/packages/core/src/index.ts
index 6a7d491..b83dc91 100644
--- a/node/packages/core/src/index.ts
+++ b/node/packages/core/src/index.ts
@@ -31,6 +31,7 @@ class Seamless {
       config?.eventObjectTransformer || ((_, outEvent) => outEvent);
 
     this.context.instance = this;
+    this.socket.on("error", (error) => this.handleErrors(error));
     this.init();
   }
 
@@ -115,7 +116,8 @@ class Seamless {
       typeof value === "string" ||
       typeof value === "number" ||
       typeof value === "boolean" ||
-      value === null
+      value === null ||
+      value === undefined
     );
   }
 
@@ -149,6 +151,10 @@ class Seamless {
     });
   }
 
+  protected handleErrors(error: any) {
+    throw new Error(error);
+  }
+
   protected serializeEventObject(event: Event) {
     const outEvent: OutEvent = {
       type: event.type,
diff --git a/seamless/__init__.py b/seamless/__init__.py
index a7b211b..28b7d25 100644
--- a/seamless/__init__.py
+++ b/seamless/__init__.py
@@ -1,9 +1,10 @@
-from .components import Component
-from .html import *
-from .rendering.html import render
+from .context import Context
 from .core import JS
+from .rendering.html import render
+from .core import Component
+from .html import *
 
-__version__ = "0.8.6"
+__version__ = "0.9.0"
 __all__ = [
     "Component",
     "A",
@@ -75,6 +76,7 @@
     "U",
     "Ul",
     "Wbr",
-    "render",
+    "Context",
     "JS",
+    "render",
 ]
diff --git a/seamless/components/__init__.py b/seamless/components/__init__.py
index 7179756..62b8eab 100644
--- a/seamless/components/__init__.py
+++ b/seamless/components/__init__.py
@@ -1,4 +1,5 @@
-from .base import Component
 from .page import Page
+from .router import Router, Route, RouterLink
 
-__all__ = ["Component", "Page"]
+
+__all__ = ["Page", "Router", "Route", "RouterLink"]
diff --git a/seamless/components/page.py b/seamless/components/page.py
index 8103f03..10a8ff1 100644
--- a/seamless/components/page.py
+++ b/seamless/components/page.py
@@ -1,8 +1,6 @@
 from typing import Iterable, Literal, TypedDict, overload
 
-from ..types import Primitive, Renderable, ChildrenType, ChildType
-
-from .base import Component
+from ..internal.utils import to_iter
 from ..html import (
     Fragment,
     Html,
@@ -11,7 +9,9 @@
     Body,
     Meta,
 )
-from ..internal import to_iter
+from ..types import Primitive, Renderable, ChildrenType, ChildType
+
+from ..core.component import Component
 
 
 class _HtmlProps(TypedDict):
diff --git a/seamless/components/router/route.py b/seamless/components/router/route.py
index 383df2c..1a1ccba 100644
--- a/seamless/components/router/route.py
+++ b/seamless/components/router/route.py
@@ -1,4 +1,4 @@
-from seamless import Component
+from ...core import Component
 
 
 class Route(Component):
diff --git a/seamless/components/router/router.js b/seamless/components/router/router.js
index 25fa477..9755927 100644
--- a/seamless/components/router/router.js
+++ b/seamless/components/router/router.js
@@ -3,6 +3,70 @@ const parent = this.parentElement;
 const PageStateChange = new Event("pageLocationChange");
 let loadingComponent = null;
 
+class IntConvertor {
+  static regex = "\\d+";
+  static convert(value) {
+    return Number(value);
+  }
+}
+
+class FloatConvertor {
+  static regex = "\\d+(\\.\\d+)?";
+
+  static convert(value) {
+    return Number(value);
+  }
+}
+
+const convertors = {
+  int: IntConvertor,
+  float: FloatConvertor,
+};
+
+routes = routes.map((route) => {
+  regex = "";
+  const regexConvertors = {};
+  for (let i = 0; i < route.path.length; i++) {
+    const key = route.path[i];
+    if (key === "?") {
+      break;
+    }
+    if (key === "{") {
+      if (route.path[i + 1] === "{") {
+        regex += "{";
+        i++;
+        continue;
+      }
+      let paramString = "";
+      while (route.path[++i] !== "}") {
+        paramString += route.path[i];
+      }
+      const [paramName, convertorType] = paramString.split(":");
+      if (!convertorType) {
+        regex += `(?<${paramName}>[^/]+)`;
+      } else {
+        const convertor = convertors[convertorType];
+        if (convertor) {
+          regex += `(?<${paramName}>${convertors[convertorType].regex})`;
+          regexConvertors[paramName] = convertors[convertorType];
+        } else {
+          throw new Error(`Unknown convertor: ${convertorType}`);
+        }
+      }
+    } else if (key === "*") {
+      regex += "(.*)";
+      break;
+    } else {
+      regex += key;
+    }
+  }
+  return {
+    regex: new RegExp(`^${regex}$`),
+    regexConvertors,
+    ...route,
+  };
+});
+
 if (loadingComponentName) {
   seamless.instance.getComponent(loadingComponentName, {}).then((component) => {
     loadingComponent = seamless.instance.toDOMElement(component);
@@ -13,28 +77,46 @@ const clearParent = () => {
   while (parent.firstChild) {
     parent.removeChild(parent.firstChild);
   }
-}
+};
 
-const loadComponent = async (name) => {
-  return seamless.instance.toDOMElement(await seamless.instance.getComponent(name, {}));
-}
+const loadComponent = async (name, props = {}) => {
+  return seamless.instance.toDOMElement(
+    await seamless.instance.getComponent(name, props)
+  );
+};
 
 window.addEventListener("pageLocationChange", () => {
   const path = window.location.pathname;
-  const page = routes.find(
-    (page) => page.path.replace(/^\//, "") === path.replace(/^\//, "")
-  );
+  let page;
+  for (let i = 0; i < routes.length; i++) {
+    const route = routes[i];
+    const match = path.match(route.regex);
+    if (match) {
+      page = route;
+      page.params = match.groups ? Object.fromEntries(
+        Object.entries(match.groups).map(([key, value]) => [
+          key,
+          route.regexConvertors[key]
+            ? route.regexConvertors[key].convert(value)
+            : value,
+        ])
+      ) : {};
+      break;
+    }
+  }
 
   if (!page) {
     return;
   }
 
+  const props = Object.fromEntries(new URLSearchParams(window.location.search).entries());
+
   clearParent();
   if (loadingComponent) {
     parent.appendChild(loadingComponent);
   }
 
-  loadComponent(page.name).then((component) => {
+  loadComponent(page.name, { ...props, ...page.params }).then((component) => {
     clearParent();
     parent.appendChild(component);
   });
@@ -44,6 +126,6 @@ seamless.navigateTo = function (to) {
   window.history.pushState({}, "", to);
   window.dispatchEvent(PageStateChange);
   return false;
-}
+};
 
 window.dispatchEvent(PageStateChange);
diff --git a/seamless/components/router/router.py b/seamless/components/router/router.py
index 51ec2b6..8f4675c 100644
--- a/seamless/components/router/router.py
+++ b/seamless/components/router/router.py
@@ -2,9 +2,7 @@
 from pathlib import Path
 from typing import overload
 
-from seamless import Component, JS
-from seamless.core import Empty
-from ...types import Renderable
+from ...core import Empty, Component, JS
 
 from .route import Route
 
@@ -35,4 +33,4 @@ def render(self):
         with open(HERE / "router.js", "r") as f:
             router_js = f.read()
 
-        return Empty(init=JS(f"const routes = {dumps(routes)};{router_js}"), loading=self.loading_component)
+        return Empty(init=JS(f"let routes = {dumps(routes)};{router_js}"), loading=self.loading_component)
diff --git a/seamless/components/router/router_link.py b/seamless/components/router/router_link.py
index 11135b4..be660d8 100644
--- a/seamless/components/router/router_link.py
+++ b/seamless/components/router/router_link.py
@@ -1,4 +1,4 @@
-from seamless import Component, A, JS
+from ... import Component, A, JS
 
 
 class RouterLink(Component):
diff --git a/seamless/context/__init__.py b/seamless/context/__init__.py
index e69de29..c599496 100644
--- a/seamless/context/__init__.py
+++ b/seamless/context/__init__.py
@@ -0,0 +1,4 @@
+
+from .context import Context, get_context
+
+__all__ = ["Context", "get_context"]
\ No newline at end of file
diff --git a/seamless/context/context.py b/seamless/context/context.py
index 5d10ba5..7e678d3 100644
--- a/seamless/context/context.py
+++ b/seamless/context/context.py
@@ -1,24 +1,71 @@
-from typing import Callable
-from seamless.types import Renderable
-from .database import ElementsDatabase
+from typing import Callable, Concatenate, ParamSpec, Any
+from socketio import AsyncServer
+
+from ..errors import ClientError
+
+from ..internal.utils import to_async, wraps
+
+from .request import WSRequest, set_request
+
+P = ParamSpec("P")
+Feature = Callable[Concatenate["Context", P], Any]
 
 
 class Context:
-    def __init__(self, *, claim_time: float) -> None:
-        self.db = ElementsDatabase(claim_time=claim_time)
+    def __init__(self) -> None:
+        """
+        :param claim_time: The time in seconds that a claim for an element is valid
+        """
+        self.server = AsyncServer(async_mode="asgi")
+        self._prop_transformers = []
+        self._post_dict_render_transformers = []
+
+    def on(self, event, handler):
+        @wraps(handler)
+        async def wrapper(sid, *args, **kwargs):
+            try:
+                WSRequest.make(sid)
+                result = await to_async(handler)(sid, *args, **kwargs)
+                set_request(None)
+                return result
+            except ClientError as e:
+                await self.server.emit("error", str(e), to=sid)
+            except Exception as e:
+                await self.server.emit("error", str(e), to=sid)
+                raise e
+
+        self.server.on(event, wrapper)
+
+    async def emit(self, event: str, *args):
+        return await self.server.emit(event, *args)
+
+    def add_feature(self, feature: Feature[P], *args: P.args, **kwargs: P.kwargs):
+        feature(self, *args, **kwargs)
+
+    def add_prop_transformer(
+        self,
+        matcher: Callable[[str, Any], bool] | str,
+        transformer: Callable[[str, Any, dict[str, Any]], None],
+    ):
+        self._prop_transformers.append((matcher, transformer))
+
+    def add_post_dict_render_transformer(
+        self, transformer: Callable[[dict[str, Any]], dict[str, Any]]
+    ):
+        self._post_dict_render_transformers.append(transformer)
+
+    @classmethod
+    def standard(cls) -> "Context":
+        context = cls()
 
-    def on(self, event: str, callback: Callable):
-        raise NotImplementedError
+        from .default import add_standard_features
 
-    def emit(self, event: str, *args):
-        raise NotImplementedError
+        add_standard_features(context)
+        return context
 
-    def render(self, component: Renderable):
-        raise NotImplementedError
 
-    def render_json(self, component: Renderable):
-        raise NotImplementedError
+_DEFAULT_CONTEXT = Context.standard()
 
-    def claim(self, claim_id: str, client_id: str):
-        self.db.claim(claim_id, client_id)
 
+def get_context(context: Context | None = None) -> Context:
+    return context or _DEFAULT_CONTEXT
diff --git a/seamless/context/default.py b/seamless/context/default.py
new file mode 100644
index 0000000..e2bc4c0
--- /dev/null
+++ b/seamless/context/default.py
@@ -0,0 +1,25 @@
+from typing import TYPE_CHECKING
+
+from ..extra.components import init_components
+from ..extra.events import init_events
+
+from ..extra.transformers.class_transformer import class_transformer
+from ..extra.transformers.dash_transformer import dash_transformer
+from ..extra.transformers.js_transformer import init_transformer, js_transformer
+from ..extra.transformers.html_events_transformer import html_events_transformer
+from ..extra.transformers.simple_transformer import simple_transformer
+
+if TYPE_CHECKING:
+    from . import Context
+
+
+def add_standard_features(ctx: "Context"):
+    ctx.add_feature(init_components)
+    ctx.add_feature(init_events, claim_time=30)
+
+    ctx.add_prop_transformer(*class_transformer())
+    ctx.add_prop_transformer(*simple_transformer())
+    ctx.add_prop_transformer(*html_events_transformer())
+    ctx.add_prop_transformer(*dash_transformer())
+    ctx.add_prop_transformer(*init_transformer())
+    ctx.add_prop_transformer(*js_transformer())
diff --git a/seamless/context/request.py b/seamless/context/request.py
index ba07604..a6cd233 100644
--- a/seamless/context/request.py
+++ b/seamless/context/request.py
@@ -2,7 +2,7 @@
 from dataclasses import dataclass
 from urllib.parse import parse_qs
 
-from seamless.internal import Cookies
+from ..internal.cookies import Cookies
 
 
 class RequestType(Enum):
@@ -62,13 +62,13 @@ def full_path(self):
         return f"{self.path}?{self._raw_query}"
 
 
-_request: WSRequest | HTTPRequest = None
+_request: WSRequest | HTTPRequest | None = None
 
 
 def request():
     return _request
 
 
-def set_request(request: Request):
+def set_request(request: WSRequest | HTTPRequest | None):
     global _request
     _request = request
diff --git a/seamless/context/ws_router.py b/seamless/context/ws_router.py
deleted file mode 100644
index 380d13a..0000000
--- a/seamless/context/ws_router.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from enum import Enum
-from seamless.rendering.json import to_dict
-from .database import DB
-from .components import COMPONENTS_REPOSITORY
-
-
-class WSRouterCommands(str, Enum):
-    GET_COMPONENT = "component"
-    EVENT = "event"
-
-
-def get_component(sid: str, name: str, props={}):
-    cls = COMPONENTS_REPOSITORY.get_component(name)
-    return to_dict(cls(**props))
-
-
-async def event(sid: str, data: str, event_data: dict):
-    await DB.invoke_event(data, event_data, scope=sid)
-
-
-ws_router = {
-    WSRouterCommands.GET_COMPONENT: get_component,
-    WSRouterCommands.EVENT: event,
-}
diff --git a/seamless/core/__init__.py b/seamless/core/__init__.py
index 9077afb..1bea950 100644
--- a/seamless/core/__init__.py
+++ b/seamless/core/__init__.py
@@ -1,10 +1,12 @@
-from .javascript import JavaScript, JS
+from .component import Component
 from .empty import Empty
+from .javascript import JavaScript, JS
 from .sid import SocketID
 
 __all__ = [
+    "Component",
+    "Empty",
     "JavaScript",
     "JS",
-    "Empty",
     "SocketID",
 ]
diff --git a/seamless/components/base.py b/seamless/core/component.py
similarity index 73%
rename from seamless/components/base.py
rename to seamless/core/component.py
index e977a5f..4627980 100644
--- a/seamless/components/base.py
+++ b/seamless/core/component.py
@@ -3,7 +3,7 @@
 
 
 if TYPE_CHECKING:
-    from seamless.types import RenderResult, ChildType
+    from ..types import RenderResult, ChildType
 
 
 class Component:
@@ -20,15 +20,9 @@ def __init__(self, *children: "ChildType") -> None:
     def render(self) -> "RenderResult":
         raise NotImplementedError(f"{type(self).__name__}.render() is not implemented")
 
-    def __init_subclass__(cls, *, name: str | None = None, **kwargs) -> None:
+    def __init_subclass__(cls, **kwargs) -> None:
         super().__init_subclass__(**kwargs)
 
-        if cls is not Component:
-            from ..context.components import COMPONENTS_REPOSITORY
-
-            cls.__seamless_name__ = name or cls.__name__
-            COMPONENTS_REPOSITORY.add_component(cls, cls.__seamless_name__)
-
         if cls.__init__ is Component.__init__:
             return
 
diff --git a/seamless/core/empty.py b/seamless/core/empty.py
index 4746fee..58ae6cf 100644
--- a/seamless/core/empty.py
+++ b/seamless/core/empty.py
@@ -1,9 +1,9 @@
 from typing import TYPE_CHECKING, Any
-from seamless.element import Element
-from seamless.internal import SEAMLESS_ELEMENT_ATTRIBUTE
+from ..element import Element
+from ..internal.constants import SEAMLESS_ELEMENT_ATTRIBUTE
 
 if TYPE_CHECKING:
-    from seamless.types import ChildType
+    from ..types import ChildType
 
 class Empty(Element):
     tag_name = "seamless:empty"
diff --git a/seamless/core/javascript.py b/seamless/core/javascript.py
index ff1c534..d7230b9 100644
--- a/seamless/core/javascript.py
+++ b/seamless/core/javascript.py
@@ -1,7 +1,5 @@
 from os import PathLike
 from typing import overload
-from seamless.internal import SEAMLESS_ELEMENT_ATTRIBUTE, SEAMLESS_INIT_ATTRIBUTE
-from seamless.rendering.transformers import transformer_for
 
 
 class JavaScript:
@@ -31,25 +29,4 @@ def __add__(self, other: "JavaScript | str") -> "JavaScript":
             )
 
 
-@transformer_for(lambda key, value: key == "init" and isinstance(value, JavaScript))
-def transform_init_source(key: str, source: JavaScript, props):
-    props[SEAMLESS_ELEMENT_ATTRIBUTE] = True
-    props[SEAMLESS_INIT_ATTRIBUTE] = props.get(SEAMLESS_INIT_ATTRIBUTE, "") + source.code
-    del props[key]
-
-
-@transformer_for(
-    lambda key, value: key.startswith("on_") and isinstance(value, JavaScript)
-)
-def transform_event_source(key: str, source: JavaScript, props):
-    event_name = key.removeprefix("on_")
-
-    props[SEAMLESS_ELEMENT_ATTRIBUTE] = True
-    props[SEAMLESS_INIT_ATTRIBUTE] = (
-        props.get(SEAMLESS_INIT_ATTRIBUTE, "")
-        + f"\nthis.addEventListener('{event_name}', (event) => {{{source.code}}});"
-    )
-    del props[key]
-
-
 JS = JavaScript
diff --git a/seamless/element.py b/seamless/element.py
index 12c517d..59e58ae 100644
--- a/seamless/element.py
+++ b/seamless/element.py
@@ -1,12 +1,11 @@
 from typing import TYPE_CHECKING, TypeVar, Generic, Unpack
 
-from .internal import to_iter
+from .internal.utils import to_iter
 from .types.html import HTMLElement
-from .rendering.props import transform_props
 
 
 if TYPE_CHECKING:
-    from seamless.types import ChildrenType, ChildType
+    from .types import ChildrenType, ChildType
 
 
 PropsType = TypeVar("PropsType", bound=HTMLElement)
@@ -26,9 +25,6 @@ def __init__(
 
     inline = False
 
-    def props_dict(self):
-        return transform_props(self.props)
-
     def __call__(self, *children: "ChildType"):
         self.children = children
         return self
diff --git a/seamless/errors.py b/seamless/errors.py
index 9d415d1..b15cbf5 100644
--- a/seamless/errors.py
+++ b/seamless/errors.py
@@ -2,17 +2,22 @@ class Error(Exception):
     """
     Base class for all seamless exceptions.
     """
+
     ...
 
+
 class RenderError(Error):
     """
     Raised when an error occurs while rendering an element.
     """
+
     ...
 
 
-class ActionError(Error):
+class ClientError(Error):
     """
-    Raised when an error occurs while invoking an action.
+    Raised when an error occurs on the client side.
+    Classes that inherit from this class will not be raised on the server side.
     """
-    ...
\ No newline at end of file
+
+    ...
diff --git a/seamless/extensions/__init__.py b/seamless/extensions/__init__.py
new file mode 100644
index 0000000..1938314
--- /dev/null
+++ b/seamless/extensions/__init__.py
@@ -0,0 +1,5 @@
+from .state import State
+
+__all__ = [
+    "State",
+]
diff --git a/seamless/extensions/state.py b/seamless/extensions/state.py
new file mode 100644
index 0000000..41334ca
--- /dev/null
+++ b/seamless/extensions/state.py
@@ -0,0 +1,5 @@
+from ..extra.state import State
+
+__all__ = [
+    "State",
+]
diff --git a/seamless/extra/__init__.py b/seamless/extra/__init__.py
index 1938314..e69de29 100644
--- a/seamless/extra/__init__.py
+++ b/seamless/extra/__init__.py
@@ -1,5 +0,0 @@
-from .state import State
-
-__all__ = [
-    "State",
-]
diff --git a/seamless/extra/components/__init__.py b/seamless/extra/components/__init__.py
new file mode 100644
index 0000000..2ac8dda
--- /dev/null
+++ b/seamless/extra/components/__init__.py
@@ -0,0 +1,41 @@
+from typing import TYPE_CHECKING
+
+from ...core.component import Component
+from ...errors import ClientError
+from ...rendering.json import to_dict
+
+from .repository import ComponentsRepository
+
+if TYPE_CHECKING:
+    from ...context import Context
+
+
+def init_components(ctx: "Context"):
+    COMPONENTS_REPOSITORY = ComponentsRepository()
+
+    original_init_subclass = Component.__init_subclass__
+
+    @classmethod
+    def __init_subclass__(
+        cls: type[Component], *, name: str | None = None, **kwargs
+    ) -> None:
+        if cls is not Component:
+
+            cls.__seamless_name__ = name or cls.__name__
+            COMPONENTS_REPOSITORY.add_component(cls, cls.__seamless_name__)
+
+        original_init_subclass.__func__(cls, **kwargs) # type: ignore
+
+    Component.__init_subclass__ = __init_subclass__
+
+    def get_component(sid: str, name: str, props=None):
+        props = props or {}
+        cls = COMPONENTS_REPOSITORY.get_component(name)
+        try:
+            component = cls(**props)
+        except TypeError:
+            raise ClientError(f"Invalid props for component {name}")
+        
+        return to_dict(component)
+
+    ctx.on("component", get_component)
diff --git a/seamless/context/components.py b/seamless/extra/components/repository.py
similarity index 81%
rename from seamless/context/components.py
rename to seamless/extra/components/repository.py
index f4a4f09..c279a58 100644
--- a/seamless/context/components.py
+++ b/seamless/extra/components/repository.py
@@ -1,7 +1,7 @@
 from typing import TYPE_CHECKING
 
 if TYPE_CHECKING:
-    from seamless import Component
+    from ...core import Component
 
 
 class ComponentsRepository:
@@ -13,6 +13,3 @@ def add_component(self, component: type["Component"], name: str):
 
     def get_component(self, component_name: str) -> "Component":
         return self.components[component_name]
-
-
-COMPONENTS_REPOSITORY = ComponentsRepository()
diff --git a/seamless/extra/events/__init__.py b/seamless/extra/events/__init__.py
new file mode 100644
index 0000000..685a953
--- /dev/null
+++ b/seamless/extra/events/__init__.py
@@ -0,0 +1,61 @@
+from typing import TYPE_CHECKING
+
+from .database import ElementsDatabase
+
+from ...internal.constants import (
+    CLAIM_COOKIE_NAME,
+    SEAMLESS_ELEMENT_ATTRIBUTE,
+    SEAMLESS_INIT_ATTRIBUTE,
+)
+from ...internal.cookies import Cookies
+
+if TYPE_CHECKING:
+    from ...context import Context
+
+
+def init_events(ctx: "Context", claim_time=20.0):
+    DB = ElementsDatabase(claim_time=claim_time)
+
+    async def event(sid: str, data: str, event_data: dict):
+        await DB.invoke_event(data, event_data, scope=sid)
+
+    async def on_connect(sid: str, env: dict):
+        cookies = Cookies(env.get("HTTP_COOKIE", ""))
+        claim_id = cookies[CLAIM_COOKIE_NAME]
+        if not claim_id:
+            return await ctx.server.disconnect(sid)
+
+        DB.claim(claim_id, sid)
+
+    async def on_disconnect(sid: str):
+        DB.release_actions(sid)
+
+    def events_transformer():
+
+        def matcher(key: str, value):
+            return key.startswith("on_") and callable(value)
+
+        def transformer(key: str, value, props):
+            event_name = key.removeprefix("on").replace("_", "").lower()
+            action = DB.add_event(value)
+            props[SEAMLESS_INIT_ATTRIBUTE] = (
+                props.get(SEAMLESS_INIT_ATTRIBUTE, "")
+                + f"""this.addEventListener('{event_name}', (event) => {{
+              const outEvent = seamless.instance.eventObjectTransformer(
+                event, 
+                seamless.instance.serializeEventObject(event)
+              );
+            seamless.instance.socket.emit("event", "{action.id}", outEvent);
+          }});"""
+            )
+
+            props[SEAMLESS_ELEMENT_ATTRIBUTE] = True
+            del props[key]
+
+        return matcher, transformer
+    
+    ctx.add_prop_transformer(*events_transformer())
+
+    ctx.on("connect", on_connect)
+    ctx.on("disconnect", on_disconnect)
+    ctx.on("event", event)
diff --git a/seamless/context/database.py b/seamless/extra/events/database.py
similarity index 91%
rename from seamless/context/database.py
rename to seamless/extra/events/database.py
index 6b8709f..0e82bf3 100644
--- a/seamless/context/database.py
+++ b/seamless/extra/events/database.py
@@ -2,13 +2,13 @@
 from typing import Any, Callable, TypeAlias
 from threading import Timer
 
-from seamless.core import SocketID
-from seamless.errors import ActionError
-from seamless.internal import wrap_with_validation
-from .request import request as _request, RequestType
+from ...core import SocketID
+from .errors import ActionError
+from ...internal.validation import wrap_with_validation
+from ...context.request import request as _request, RequestType
 
 
-ActionsMap: TypeAlias = dict[str, Callable] | dict[str, dict[str, Callable]]
+ActionsMap: TypeAlias = dict[str, Callable | dict[str, Callable]]
 
 
 class Action:
@@ -18,7 +18,7 @@ def __init__(
         id: str,
     ):
         self.id = id
-        self.action = action  # TODO: make wrapper for action for data validation
+        self.action = action
 
     async def __call__(self, *args: Any, **kwargs: Any) -> Any:
         if iscoroutinefunction(self.action):
@@ -78,7 +78,7 @@ async def invoke_event(self, event: str, *data, scope=None):
             func = self.events[event]
         else:
             raise ActionError("Event not found")
-        
+
         data = list(data)
         for index, param in enumerate(signature(func.action).parameters.values()):
             if param.annotation is SocketID:
diff --git a/seamless/extra/events/errors.py b/seamless/extra/events/errors.py
new file mode 100644
index 0000000..6740047
--- /dev/null
+++ b/seamless/extra/events/errors.py
@@ -0,0 +1,7 @@
+from ...errors import Error
+
+class ActionError(Error):
+    """
+    Raised when an error occurs while invoking an action.
+    """
+    ...
\ No newline at end of file
diff --git a/seamless/extra/state/__init__.py b/seamless/extra/state/__init__.py
index 9a22922..9a092e7 100644
--- a/seamless/extra/state/__init__.py
+++ b/seamless/extra/state/__init__.py
@@ -2,10 +2,10 @@
 from json import dumps
 from typing import Any, overload
 
-from seamless import Component, JS
-from seamless.core import Empty
-from seamless.internal import SEAMLESS_ELEMENT_ATTRIBUTE, SEAMLESS_INIT_ATTRIBUTE
-from seamless.rendering.transformers import transformer_for
+from ...core.component import Component
+from ...core import Empty, JS
+from ...internal.constants import SEAMLESS_ELEMENT_ATTRIBUTE, SEAMLESS_INIT_ATTRIBUTE
+from ...extra.transformers import transformer_for
 
 HERE = Path(__file__).parent
 EMPTY = object()
@@ -40,6 +40,9 @@ def set(self, value):
             f"""const state = seamless.state.getState('{self.name}');\
             seamless.state.setState('{self.name}', {value})"""
         )
+    
+    def __str__(self):
+        return f"seamless.state.getState('{self.name}')"
 
     @overload
     def __call__(self) -> Any: ...
diff --git a/seamless/rendering/transformers/__init__.py b/seamless/extra/transformers/__init__.py
similarity index 61%
rename from seamless/rendering/transformers/__init__.py
rename to seamless/extra/transformers/__init__.py
index c2ab5a7..b753240 100644
--- a/seamless/rendering/transformers/__init__.py
+++ b/seamless/extra/transformers/__init__.py
@@ -1,21 +1,9 @@
-from typing import Any, Callable
+from typing import Any, Callable, Concatenate, TYPE_CHECKING
 
-from .simple_transformer import simple_transformer as _simple_transformer
-from .events_transformer import (
-    events_transformer as _events_transformer,
-    js_events_transformer as _js_events_transformer,
-)
-from .class_transformer import class_transformer as _class_transformer
+if TYPE_CHECKING:
+    from ...context.context import Context
 
-
-TRANSFORMERS = [
-    _simple_transformer(),
-    _events_transformer(),
-    _js_events_transformer(),
-]
-
-
-def transformer_for(matcher: Callable[[str, Any], bool] | str):
+def transformer_for(matcher: Callable[[str, Any], bool] | str, context: "Context | None" = None):
     """
     A decorator to register a prop transformer.
 
@@ -43,12 +31,11 @@ def transformer_for(matcher: Callable[[str, Any], bool] | str):
         ...     element_props["class"] = " ".join(str(class_name).split())
 
     """
+    from ...context.context import get_context
+    context = get_context(context)
 
-    def decorator(func: Callable[[str, Any, dict[str, Any]], None]):
-        TRANSFORMERS.append((matcher, func))
+    def decorator(func: Callable[Concatenate[str, Any, dict[str, Any], ...], None]):
+        context.add_prop_transformer(matcher, func)
         return func
 
     return decorator
-
-
-transformer_for("class_name")(_class_transformer)
diff --git a/seamless/extra/transformers/class_transformer.py b/seamless/extra/transformers/class_transformer.py
new file mode 100644
index 0000000..c1291b9
--- /dev/null
+++ b/seamless/extra/transformers/class_transformer.py
@@ -0,0 +1,12 @@
+def class_transformer():
+    def matcher(key, value):
+        return key == "class_name"
+
+    def transformer(key, class_name, element_props):
+        if isinstance(class_name, list):
+            class_name = " ".join(class_name)
+
+        element_props["class"] = " ".join(str(class_name).split())
+        del element_props["class_name"]
+
+    return matcher, transformer
\ No newline at end of file
diff --git a/seamless/extra/transformers/dash_transformer.py b/seamless/extra/transformers/dash_transformer.py
new file mode 100644
index 0000000..f01194c
--- /dev/null
+++ b/seamless/extra/transformers/dash_transformer.py
@@ -0,0 +1,10 @@
+def dash_transformer():
+    def matcher(key: str, value):
+        return "_" in key and (isinstance(value, (str, int, float)) or value is None)
+    
+    def transformer(key, value, props):
+        dash_key = key.replace("-", "_")
+        props[dash_key] = value
+        del props[key]
+
+    return matcher, transformer
\ No newline at end of file
diff --git a/seamless/extra/transformers/html_events_transformer.py b/seamless/extra/transformers/html_events_transformer.py
new file mode 100644
index 0000000..2a6d45d
--- /dev/null
+++ b/seamless/extra/transformers/html_events_transformer.py
@@ -0,0 +1,10 @@
+def html_events_transformer():
+    def matcher(key: str, value):
+        return key.startswith("on_") and isinstance(value, str)
+
+    def transformer(key: str, value, element_props):       
+        event_name = key.replace("_", "").lower()
+        element_props[event_name] = value
+        del element_props[key]
+
+    return matcher, transformer
\ No newline at end of file
diff --git a/seamless/extra/transformers/js_transformer.py b/seamless/extra/transformers/js_transformer.py
new file mode 100644
index 0000000..2c81dd7
--- /dev/null
+++ b/seamless/extra/transformers/js_transformer.py
@@ -0,0 +1,33 @@
+from ...core import JavaScript
+from ...internal.constants import SEAMLESS_ELEMENT_ATTRIBUTE, SEAMLESS_INIT_ATTRIBUTE
+
+
+def init_transformer():
+    def matcher(key: str, value):
+        return key == "init" and isinstance(value, JavaScript)
+
+    def transformer(key: str, source: JavaScript, props):
+        props[SEAMLESS_ELEMENT_ATTRIBUTE] = True
+        props[SEAMLESS_INIT_ATTRIBUTE] = (
+            props.get(SEAMLESS_INIT_ATTRIBUTE, "") + source.code
+        )
+        del props[key]
+
+    return matcher, transformer
+
+
+def js_transformer():
+    def matcher(key: str, value):
+        return key.startswith("on_") and isinstance(value, JavaScript)
+
+    def transformer(key: str, source: JavaScript, props):
+        event_name = key.removeprefix("on_")
+
+        props[SEAMLESS_ELEMENT_ATTRIBUTE] = True
+        props[SEAMLESS_INIT_ATTRIBUTE] = (
+            props.get(SEAMLESS_INIT_ATTRIBUTE, "")
+            + f"\nthis.addEventListener('{event_name}', (event) => {{{source.code}}});"
+        )
+        del props[key]
+
+    return matcher, transformer
diff --git a/seamless/rendering/transformers/simple_transformer.py b/seamless/extra/transformers/simple_transformer.py
similarity index 100%
rename from seamless/rendering/transformers/simple_transformer.py
rename to seamless/extra/transformers/simple_transformer.py
diff --git a/seamless/html/img.py b/seamless/html/img.py
index dc3a09b..25672b4 100644
--- a/seamless/html/img.py
+++ b/seamless/html/img.py
@@ -2,11 +2,11 @@
 from ..element import Element
 
 if TYPE_CHECKING:
-    from ..types.html import HTMLElementProps
+    from ..types.html import HTMLImageElement
     from ..types import ChildType
 
 class Img(Element):
-    def __init__(self, *children: "ChildType", **kwargs: Unpack["HTMLElementProps"]):
+    def __init__(self, *children: "ChildType", **kwargs: Unpack["HTMLImageElement"]):
         super().__init__(*children, **kwargs)
 
     tag_name = "img"
diff --git a/seamless/internal.py b/seamless/internal.py
deleted file mode 100644
index 9670bd5..0000000
--- a/seamless/internal.py
+++ /dev/null
@@ -1,147 +0,0 @@
-# type: ignore
-
-from functools import wraps
-from typing import Any, Iterable
-from uuid import uuid4
-from string import ascii_letters
-
-ascii_length = len(ascii_letters)
-
-
-SEAMLESS_ELEMENT_ATTRIBUTE = "seamless:element"
-SEAMLESS_INIT_ATTRIBUTE = "seamless:init"
-
-
-class _DataValidationError(Exception): ...
-
-
-class Cookies:
-    def __init__(self, cookie_string: str):
-        self.cookies = dict[str, str]()
-        self._parse(cookie_string)
-
-    def _parse(self, cookie_string: str):
-        if not cookie_string:
-            return
-
-        for cookie in cookie_string.split(";"):
-            key, value = cookie.split("=", maxsplit=1)
-            self.cookies[key.strip()] = value.strip()
-
-    def __getitem__(self, key: str):
-        return self.cookies[key] if key in self.cookies else None
-
-    def __contains__(self, key: str):
-        return key in self.cookies
-
-    @staticmethod
-    def from_request_headers(headers: Iterable[Iterable[bytes]] | dict):
-        cookie_string = ""
-        if isinstance(headers, dict):
-            return Cookies(headers.get("cookie", ""))
-
-        for header, value in headers:
-            if header == b"cookie":
-                cookie_string = value.decode()
-                break
-        return Cookies(cookie_string)
-
-
-class TwoWayDict(dict):
-    def __setitem__(self, key, value):
-        if key in self:
-            del self[key]
-        if value in self:
-            del self[value]
-        dict.__setitem__(self, key, value)
-        dict.__setitem__(self, value, key)
-
-    def __delitem__(self, key):
-        dict.__delitem__(self, self[key])
-        dict.__delitem__(self, key)
-
-    def __len__(self):
-        """Returns the number of connections"""
-        return dict.__len__(self) // 2
-
-
-class Promise:
-    def __init__(self, value):
-        self.value = value
-
-    def __await__(self):
-        return getattr(self.value, "__await__", lambda: self.value or iter(()))()
-
-
-def short_uuid(length=12):
-    original_uuid = uuid4()
-    hex_string = original_uuid.hex
-    base62_uuid = ""
-    for i in range(0, len(hex_string), 2):
-        hex_byte = int(hex_string[i : i + 2], 16)
-        base62_uuid += ascii_letters[hex_byte % ascii_length]
-
-    short_uuid = base62_uuid[:length]
-
-    return short_uuid
-
-
-def wrap_with_validation(func):
-    def no_validation(*args):
-        return func(*[_obj(arg) if isinstance(arg, dict) else arg for arg in args])
-
-    try:
-        from pydantic import create_model, ValidationError
-    except ImportError:
-        return no_validation
-
-    import inspect
-
-    signature = inspect.signature(func)
-    parameters = signature.parameters
-    if not parameters:
-        return no_validation
-
-    func_parameters = {
-        name: (
-            parameter.annotation if parameter.annotation is not inspect._empty else Any,
-            parameter.default if parameter.default is not inspect._empty else None,
-        )
-        for name, parameter in parameters.items()
-    }
-
-    model = create_model(
-        "SeamlessModel",
-        **func_parameters,
-    )
-
-    @wraps(func)
-    async def wrapper(*args):
-        kwargs = {parameter: args[i] for i, parameter in enumerate(func_parameters)}
-
-        try:
-            data = model(**kwargs)
-        except ValidationError as e:
-            raise _DataValidationError(e.json(include_url=False))
-
-        return await Promise(
-            func(**{name: getattr(data, name) for name in func_parameters})
-        )
-
-    return wrapper
-
-
-def to_iter(value):
-    try:
-        return iter(value)
-    except TypeError:
-        return iter((value,))
-
-
-class _obj(object):
-    def __init__(self, d: dict):
-        for key, value in d.items():
-            if isinstance(key, (list, tuple)):
-                setattr(self, key, [_obj(x) if isinstance(x, dict) else x for x in value])
-            else:
-                setattr(self, key, _obj(value) if isinstance(value, dict) else value)
diff --git a/seamless/rendering/context.py b/seamless/internal/__init__.py
similarity index 100%
rename from seamless/rendering/context.py
rename to seamless/internal/__init__.py
diff --git a/seamless/internal/constants.py b/seamless/internal/constants.py
new file mode 100644
index 0000000..4bd75a6
--- /dev/null
+++ b/seamless/internal/constants.py
@@ -0,0 +1,4 @@
+SEAMLESS_ELEMENT_ATTRIBUTE = "seamless:element"
+SEAMLESS_INIT_ATTRIBUTE = "seamless:init"
+
+CLAIM_COOKIE_NAME = "_seamless_claim_id"
\ No newline at end of file
diff --git a/seamless/internal/cookies.py b/seamless/internal/cookies.py
new file mode 100644
index 0000000..1f5524c
--- /dev/null
+++ b/seamless/internal/cookies.py
@@ -0,0 +1,34 @@
+# type: ignore
+
+from typing import Iterable
+
+class Cookies:
+    def __init__(self, cookie_string: str):
+        self.cookies = dict[str, str]()
+        self._parse(cookie_string)
+
+    def _parse(self, cookie_string: str):
+        if not cookie_string:
+            return
+
+        for cookie in cookie_string.split(";"):
+            key, value = cookie.split("=", maxsplit=1)
+            self.cookies[key.strip()] = value.strip()
+
+    def __getitem__(self, key: str):
+        return self.cookies[key] if key in self.cookies else None
+
+    def __contains__(self, key: str):
+        return key in self.cookies
+
+    @staticmethod
+    def from_request_headers(headers: Iterable[Iterable[bytes]] | dict):
+        cookie_string = ""
+        if isinstance(headers, dict):
+            return Cookies(headers.get("cookie", ""))
+
+        for header, value in headers:
+            if header == b"cookie":
+                cookie_string = value.decode()
+                break
+        return Cookies(cookie_string)
diff --git a/seamless/internal/injector.py b/seamless/internal/injector.py
new file mode 100644
index 0000000..a9f556d
--- /dev/null
+++ b/seamless/internal/injector.py
@@ -0,0 +1,18 @@
+from functools import wraps
+from inspect import iscoroutinefunction, signature
+from typing import Type, TypeVar, overload, Callable
+
+T = TypeVar('T')
+
+class Injector:
+  def __init__(self):
+    self.dependencies = dict[type, Callable]()
+
+
+  @overload
+  def add(self, cls: Type[T], factory: Callable[[], T], /) -> None: ...
+  @overload
+  def add(self, cls: Type[T], instance: T, /) -> None: ...
+
+  def add(self, cls: Type[T], dependency: Callable[[], T] | T, /) -> None:
+    self.dependencies[cls] = dependency if callable(dependency) else lambda: dependency
diff --git a/seamless/internal/utils.py b/seamless/internal/utils.py
new file mode 100644
index 0000000..66f0fee
--- /dev/null
+++ b/seamless/internal/utils.py
@@ -0,0 +1,110 @@
+# type: ignore
+
+from functools import wraps as _wraps
+from inspect import iscoroutinefunction, ismethod
+from string import ascii_letters
+from uuid import uuid4
+
+ascii_length = len(ascii_letters)
+
+
+class Promise:
+    def __init__(self, value):
+        self.value = value
+
+    def __await__(self):
+        return getattr(self.value, "__await__", lambda: self.value or iter(()))()
+
+
+class TwoWayDict(dict):
+    def __setitem__(self, key, value):
+        if key in self:
+            del self[key]
+        if value in self:
+            del self[value]
+        dict.__setitem__(self, key, value)
+        dict.__setitem__(self, value, key)
+
+    def __delitem__(self, key):
+        dict.__delitem__(self, self[key])
+        dict.__delitem__(self, key)
+
+    def __len__(self):
+        """Returns the number of connections"""
+        return dict.__len__(self) // 2
+
+
+def short_uuid(length=12):
+    original_uuid = uuid4()
+    hex_string = original_uuid.hex
+    base62_uuid = ""
+    for i in range(0, len(hex_string), 2):
+        hex_byte = int(hex_string[i : i + 2], 16)
+        base62_uuid += ascii_letters[hex_byte % ascii_length]
+
+    short_uuid = base62_uuid[:length]
+
+    return short_uuid
+
+
+def to_iter(value):
+    try:
+        return iter(value)
+    except TypeError:
+        return iter((value,))
+
+
+def to_async(func):
+    if iscoroutinefunction(func):
+        return func
+
+    @wraps(func)
+    async def wrapper(*args, **kwargs):
+        return func(*args, **kwargs)
+
+    return wrapper
+
+
+class _obj(object):
+    def __init__(self, d: dict):
+        for key, value in d.items():
+            if isinstance(key, (list, tuple)):
+                setattr(
+                    self, key, [_obj(x) if isinstance(x, dict) else x for x in value]
+                )
+            else:
+                setattr(self, key, _obj(value) if isinstance(value, dict) else value)
+
+
+def wraps(info):
+    def inner(callback):
+        if ismethod(info):
+
+            class _Wrapper:
+                if iscoroutinefunction(callback):
+
+                    @_wraps(info)
+                    async def wrapper(self, *args, **kwargs):
+                        return await callback(*args, **kwargs)
+
+                else:
+
+                    @_wraps(info)
+                    def wrapper(self, *args, **kwargs):
+                        return callback(*args, **kwargs)
+
+            return _Wrapper().wrapper
+
+        if iscoroutinefunction(callback):
+
+            @_wraps(info)
+            async def wrapper(*args, **kwargs):
+                return await callback(*args, **kwargs)
+
+            return wrapper
+
+        @_wraps(info)
+        def wrapper(*args, **kwargs):
+            return callback(*args, **kwargs)
+
+    return inner
diff --git a/seamless/internal/validation.py b/seamless/internal/validation.py
new file mode 100644
index 0000000..065f396
--- /dev/null
+++ b/seamless/internal/validation.py
@@ -0,0 +1,54 @@
+# type: ignore
+
+from typing import Any
+from .utils import Promise, _obj, wraps
+from ..errors import ClientError
+
+
+class _DataValidationError(ClientError): ...
+
+
+def wrap_with_validation(func):
+    @wraps(func)
+    def no_validation(*args):
+        return func(*[_obj(arg) if isinstance(arg, dict) else arg for arg in args])
+
+    try:
+        from pydantic import create_model, ValidationError
+    except ImportError:
+        return no_validation
+
+    import inspect
+
+    signature = inspect.signature(func)
+    parameters = signature.parameters
+    if not parameters:
+        return no_validation
+
+    func_parameters = {
+        name: (
+            parameter.annotation if parameter.annotation is not inspect._empty else Any,
+            parameter.default if parameter.default is not inspect._empty else None,
+        )
+        for name, parameter in parameters.items()
+    }
+
+    model = create_model(
+        "SeamlessModel",
+        **func_parameters,
+    )
+
+    @wraps(func)
+    async def wrapper(*args):
+        kwargs = {parameter: args[i] for i, parameter in enumerate(func_parameters)}
+
+        try:
+            data = model(**kwargs)
+        except ValidationError as e:
+            raise _DataValidationError(e.json(include_url=False))
+
+        return await Promise(
+            func(**{name: getattr(data, name) for name in func_parameters})
+        )
+
+    return wrapper
diff --git a/seamless/middlewares/asgi.py b/seamless/middlewares/asgi.py
index 9522394..3db0020 100644
--- a/seamless/middlewares/asgi.py
+++ b/seamless/middlewares/asgi.py
@@ -1,11 +1,11 @@
-from functools import partial
-from socketio import AsyncServer, ASGIApp
+from socketio import ASGIApp
 
-from .base import BaseAsyncMiddleware, CLAIM_COOKIE_NAME
+from .base import BaseMiddleware
 from ..context.request import HTTPRequest
+from ..internal.constants import CLAIM_COOKIE_NAME
 
 
-class ASGIMiddleware(BaseAsyncMiddleware):
+class ASGIMiddleware(BaseMiddleware):
     async def __call__(self, scope, receive, send):
         async def _send(message):
             await send(message)
@@ -40,6 +40,3 @@ async def _send(message):
 
     def _app_class(self):
         return ASGIApp
-
-    def _server_class(self):
-        return partial(AsyncServer, async_mode="asgi")
diff --git a/seamless/middlewares/base.py b/seamless/middlewares/base.py
index e42d1d5..88d049c 100644
--- a/seamless/middlewares/base.py
+++ b/seamless/middlewares/base.py
@@ -1,90 +1,17 @@
-from functools import wraps
-from pathlib import Path
-from inspect import iscoroutinefunction
-
-import asyncio
-
-from socketio.base_server import BaseServer
-from socketio import AsyncServer
-
-from ..context.database import DB
-from ..context.ws_router import ws_router
-from ..context.request import WSRequest, request as _request, set_request
-
-from ..internal import Cookies, _DataValidationError
-
-
-CLAIM_COOKIE_NAME = "_seamless_claim_id"
+from ..context.context import Context, get_context
+from ..context.request import request as _request
 
 
 class BaseMiddleware:
-    def __init__(self, app, socket_path="/socket.io"):
+    def __init__(self, app, socket_path="/socket.io", context: Context | None = None):
+        if context is None:
+            context = get_context()
         self.socket_path = f"/{socket_path.strip('/')}"
-        self.server = self._server_class()(cors_allowed_origins=[])
-        self.app = self._app_class()(self.server, app, socketio_path=socket_path)
-        self._disconnect = self._make_method(self.server.disconnect)
-        self._setup()
-
-    def _setup(self):
-        self._setup_events()
-
-    def _emit(self, *args, **kwargs):
-        return self.server.emit(*args, **kwargs)
-
-    def _setup_events(self):
-        self.server.on("disconnect", self._handle_disconnect)
-        self.server.on("connect", self._handle_connect)
-
-        for command, handler in ws_router.items():
-            wrapped = self._make_handler(handler)
-            self.on(command, wrapped)
-
-    def _make_handler(self, handler):
-        def _handler(sid, *data):
-            return handler(sid, *data)
-
-        return _handler
-
-    def _handle_disconnect(self, sid: str):
-        DB.release_actions(sid)
-
-    def _handle_connect(self, sid: str, env):
-        cookie_string = env.get("HTTP_COOKIE", "")
-        if not cookie_string:
-            self._disconnect(sid)
-
-        cookies = Cookies(env.get("HTTP_COOKIE", ""))
-        claim_id = cookies[CLAIM_COOKIE_NAME]
-        if not claim_id:
-            self._disconnect(sid)
-
-        DB.claim(claim_id, sid)
-
-    def on(self, event, handler):
-        @wraps(handler)
-        def wrapper(sid, *args, **kwargs):
-            try:
-                WSRequest.make(sid)
-                result = handler(sid, *args, **kwargs)
-                set_request(None)
-                return result
-            except _DataValidationError as e:
-                self._emit("error", str(e), to=sid)
-            except Exception as e:
-                self._emit("error", str(e), to=sid)
-                raise e
-
-        self.server.on(event, wrapper)
+        self.app = self._app_class()(context.server, app, socketio_path=socket_path)
 
     def _app_class(self):
         raise NotImplementedError("self._app_class is not implemented")
 
-    def _server_class(self) -> type[BaseServer]:
-        raise NotImplementedError("self._server_class is not implemented")
-
-    def _is_async_server(self) -> bool:
-        return False
-
     def _make_cookie_header(
         self,
         name: str,
@@ -126,67 +53,8 @@ def _remove_cookie(self, name: str, path: str | None = None):
             f"{name}=; Path={path}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;".encode(),
         )
 
-    def _make_method(self, method):
-        if self._is_async_server():
-
-            def _method(*args, **kwargs):
-                if iscoroutinefunction(method):
-                    return asyncio.get_running_loop().create_task(
-                        method(*args, **kwargs)
-                    )
-                else:
-
-                    async def _wrapper():
-                        return method(*args, **kwargs)
-
-                    return _wrapper()
-
-        else:
-
-            def _method(*args, **kwargs):
-                return method(*args, **kwargs)
-
-        return _method
-
     def _is_render_request(self):
         request = _request()
         if not request:
             return False
         return request.id is not None
-
-
-class BaseAsyncMiddleware(BaseMiddleware):
-    server: AsyncServer
-
-    def _is_async_server(self) -> bool:
-        return True
-
-    async def _emit(self, *args, **kwargs):
-        return await self.server.emit(*args, **kwargs)
-
-    def _make_handler(self, handler):
-        async def _handler(sid, *data):
-            if iscoroutinefunction(handler):
-                result = await handler(sid, *data)
-            else:
-                result = handler(sid, *data)
-
-            return result
-
-        return _handler
-
-    def on(self, event, handler):
-        @wraps(handler)
-        async def wrapper(sid, *args, **kwargs):
-            try:
-                WSRequest.make(sid)
-                result = await handler(sid, *args, **kwargs)
-                set_request(None)
-                return result
-            except _DataValidationError as e:
-                await self.server.emit("error", str(e), to=sid)
-            except Exception as e:
-                await self.server.emit("error", str(e), to=sid)
-                raise e
-
-        self.server.on(event, wrapper)
diff --git a/seamless/rendering/html.py b/seamless/rendering/html.py
index 2b81f2a..89eec30 100644
--- a/seamless/rendering/html.py
+++ b/seamless/rendering/html.py
@@ -1,17 +1,18 @@
 from typing import TYPE_CHECKING
 from uuid import uuid4 as uuid
 
+from ..context import Context, get_context
 from ..context.request import request as _request
 from ..errors import RenderError
-from ..components.base import Component
+from ..core.component import Component
 from ..element import Element
-from .props import render_props
+from .props import render_props, transform_props
 
 if TYPE_CHECKING:
-    from seamless.types import Renderable, Primitive
+    from ..types import Renderable, Primitive
 
 
-def render(element: "Renderable | Primitive", *, pretty=False, tab_indent=1) -> str:
+def render(element: "Renderable | Primitive", *, pretty=False, tab_indent=1, context: "Context | None" = None) -> str:
     """
     Renders the given element into an HTML string.
 
@@ -26,19 +27,19 @@ def render(element: "Renderable | Primitive", *, pretty=False, tab_indent=1) ->
     request = _request()
     if request is not None:
         request.id = str(uuid())
-    return _render(element, pretty=pretty, tab_indent=tab_indent)
+    return _render(element, pretty=pretty, tab_indent=tab_indent, context=get_context(context))
 
 
-def _render(element: "Renderable | Primitive", *, pretty=False, tab_indent=1) -> str:
+def _render(element: "Renderable | Primitive", *, pretty=False, tab_indent=1, context: "Context") -> str:
     if isinstance(element, Component):
-        element = _render(element.render(), pretty=pretty, tab_indent=tab_indent)
+        element = _render(element.render(), pretty=pretty, tab_indent=tab_indent, context=context)
 
     if not isinstance(element, Element):
         return str(element) if element is not None else ""
 
     tag_name = getattr(element, "tag_name", None)
 
-    props = {k: v for k, v in element.props_dict().items() if v not in [None, False]}
+    props = {k: v for k, v in transform_props(element.props, context=context).items() if v not in [None, False]}
 
     props_string = render_props(props)
     open_tag = f"{tag_name} {props_string}".strip()
@@ -52,7 +53,7 @@ def _render(element: "Renderable | Primitive", *, pretty=False, tab_indent=1) ->
     tab = "  " * tab_indent if pretty else ""
     children_join_string = f"\n{tab}" if pretty else ""
     children = [
-        _render(child, pretty=pretty, tab_indent=tab_indent + 1)
+        _render(child, pretty=pretty, tab_indent=tab_indent + 1, context=context)
         for child in element.children
     ]
     if pretty:
diff --git a/seamless/rendering/json.py b/seamless/rendering/json.py
index 02368f2..0d5ecdb 100644
--- a/seamless/rendering/json.py
+++ b/seamless/rendering/json.py
@@ -1,14 +1,23 @@
 from typing import TYPE_CHECKING
 
-from ..components import Component
+from ..core.component import Component
 from ..element import Element
+from .props import transform_props
 
 if TYPE_CHECKING:
-    from seamless.types import Renderable, Primitive
+    from ..types import Renderable, Primitive
+    from ..context import Context
 
-def to_dict(element: "Renderable | Primitive"):
+
+def to_dict(element: "Renderable | Primitive", *, context: "Context | None" = None):
+    from ..context import get_context
+
+    return _to_dict(element, context=get_context(context))
+
+
+def _to_dict(element: "Renderable | Primitive", *, context: "Context"):
     if isinstance(element, Component):
-        element = to_dict(element.render())
+        element = _to_dict(element.render(), context=context)
 
     if not isinstance(element, Element):
         return element
@@ -21,6 +30,5 @@ def to_dict(element: "Renderable | Primitive"):
                 element.children,
             )
         ),
-        "props": element.props_dict(),
+        "props": transform_props(element.props, context=context),
     }
-
diff --git a/seamless/rendering/props.py b/seamless/rendering/props.py
index c47329e..e5359f2 100644
--- a/seamless/rendering/props.py
+++ b/seamless/rendering/props.py
@@ -1,13 +1,15 @@
 from html import escape
-from typing import Any
+from typing import Any, TYPE_CHECKING
 from ..errors import RenderError
-from .transformers import TRANSFORMERS
 
+if TYPE_CHECKING:
+    from ..context import Context
 
-def transform_props(props: dict[str, Any]):
+
+def transform_props(props: dict[str, Any], *, context: "Context"):
     props_copy = props.copy()
 
-    for matcher, transformer in TRANSFORMERS:
+    for matcher, transformer in context._prop_transformers:
         if isinstance(matcher, str):
             key = matcher
             if key in props_copy:
diff --git a/seamless/rendering/transformers/aria_transformer.py b/seamless/rendering/transformers/aria_transformer.py
deleted file mode 100644
index 6c46bc1..0000000
--- a/seamless/rendering/transformers/aria_transformer.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from . import transformer_for
-
-@transformer_for(lambda key, _: key.startswith("aria_"))
-def aria_transformer(key, value, props):
-    aria_key = key.replace("_", "-")
-    props[aria_key] = value
-    del props[key]
\ No newline at end of file
diff --git a/seamless/rendering/transformers/class_transformer.py b/seamless/rendering/transformers/class_transformer.py
deleted file mode 100644
index 79f7813..0000000
--- a/seamless/rendering/transformers/class_transformer.py
+++ /dev/null
@@ -1,6 +0,0 @@
-def class_transformer(key, class_name, element_props):
-    if isinstance(class_name, list):
-        class_name = " ".join(class_name)
-
-    element_props["class"] = " ".join(str(class_name).split())
-    del element_props["class_name"]
diff --git a/seamless/rendering/transformers/events_transformer.py b/seamless/rendering/transformers/events_transformer.py
deleted file mode 100644
index f575ef0..0000000
--- a/seamless/rendering/transformers/events_transformer.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from seamless.internal import SEAMLESS_ELEMENT_ATTRIBUTE, SEAMLESS_INIT_ATTRIBUTE
-
-def events_transformer():
-
-    def matcher(key: str, value):
-        return key.startswith("on_") and callable(value)
-
-    def event_transformer(key: str, value, props):
-        from seamless.context.database import DB
-
-        if not callable(value):
-            props[key] = value
-            return
-        
-        event_name = key.removeprefix("on_")
-        action = DB.add_event(value)
-        props[SEAMLESS_INIT_ATTRIBUTE] = props.get(SEAMLESS_INIT_ATTRIBUTE, "") + \
-        f"""this.addEventListener('{event_name}', (event) => {{
-            const outEvent = seamless.instance.eventObjectTransformer(
-              event, 
-              seamless.instance.serializeEventObject(event)
-            );
-          seamless.instance.socket.emit("event", "{action.id}", outEvent);
-        }});"""
-
-        props[SEAMLESS_ELEMENT_ATTRIBUTE] = True
-        del props[key]
-
-    return matcher, event_transformer
-
-def js_events_transformer():
-    def matcher(key: str, value):
-        return key.startswith("on_") and isinstance(value, str)
-
-    def event_transformer(key: str, value, element_props):       
-        event_name = key.removeprefix("on_")
-        element_props[f"on{event_name}"] = value
-        del element_props[key]
-
-    return matcher, event_transformer
\ No newline at end of file
diff --git a/seamless/styling/css_modules.py b/seamless/styling/css_modules.py
index 8ff98f2..d910e41 100644
--- a/seamless/styling/css_modules.py
+++ b/seamless/styling/css_modules.py
@@ -3,7 +3,7 @@
 from pathlib import Path
 import re
 import cssutils
-from seamless.internal import short_uuid
+from ..internal.utils import short_uuid
 
 
 class CSSClass:
diff --git a/seamless/styling/style.py b/seamless/styling/style.py
index fc69983..85e3cfb 100644
--- a/seamless/styling/style.py
+++ b/seamless/styling/style.py
@@ -1,8 +1,8 @@
 from typing import Generic, TypeVar, Unpack, TYPE_CHECKING
-from seamless.rendering.transformers import transformer_for
+from ..extra.transformers import transformer_for
 
 if TYPE_CHECKING:
-    from seamless.types.styling.css_properties import CSSProperties
+    from ..types.styling.css_properties import CSSProperties
 
 T = TypeVar("T")
 
diff --git a/seamless/types/__init__.py b/seamless/types/__init__.py
index 86ba1c9..e687851 100644
--- a/seamless/types/__init__.py
+++ b/seamless/types/__init__.py
@@ -1,8 +1,8 @@
 from typing import Collection, TypeAlias, TYPE_CHECKING, Union
 
 if TYPE_CHECKING:
-    from seamless import Component
-    from seamless.element import Element
+    from ..core import Component
+    from ..element import Element
 
 
 Primitive: TypeAlias = str | int | float | bool | None


Test patch:
diff --git a/tests/server/asgi.py b/tests/server/asgi.py
index 6f173d0..9f03d21 100644
--- a/tests/server/asgi.py
+++ b/tests/server/asgi.py
@@ -2,10 +2,10 @@
 
 from fastapi import FastAPI
 from fastapi.middleware.cors import CORSMiddleware
-from fastapi.responses import HTMLResponse, JSONResponse, Response
+from fastapi.responses import HTMLResponse, Response
 
 from seamless.middlewares import ASGIMiddleware
-from .common import index, db_memory, css_file
+from .common import index, css_file
 
 HERE = Path(__file__).parent
 
@@ -24,5 +24,4 @@
 )
 
 app.get("/c", response_class=HTMLResponse)(index)
-app.get("/usage", response_class=JSONResponse)(db_memory)
 app.get("/static/main.css")(lambda: Response(css_file(), media_type="text/css"))
diff --git a/tests/server/common.py b/tests/server/common.py
index 7341f5c..a5d1e6b 100644
--- a/tests/server/common.py
+++ b/tests/server/common.py
@@ -2,8 +2,6 @@
 from seamless import Component, render
 from seamless.components import Page as _Page
 from seamless.styling import CSS, StyleObject
-from seamless.context.database import DB
-from seamless.types.events import MouseEvent
 
 
 def index():
@@ -14,14 +12,6 @@ def css_file(minified=False):
     return CSS.to_css_string(minified)
 
 
-def db_memory():
-    return {
-        "elements": len(DB.elements),
-        "ids": len(DB.element_ids),
-        "unclaimed": list(DB._all_unclaimed.keys()),
-    }
-
-
 class Page(_Page):
     def head(self):
         yield from super().head()