Component Registry¶
The registry is how PyJinHx tracks component instances, enabling cross-referencing between components in templates.
How It Works¶
Components are automatically registered at two points in their lifecycle:
Registration Lifecycle¶
-
Class Definition (
__init_subclass__) -
Instance Creation (
__init__)
This happens transparently—you don't need to call any registration methods manually.
Composite Keys¶
The registry stores components using a composite key of ComponentName_id. This means:
- A
Buttonwithid="main"is stored asButton_main - A
Cardwithid="main"is stored asCard_main
Tip
Different component types can share the same id without collision.
Registry Scoping¶
The Problem¶
In web applications, component instances from one request can persist and affect subsequent requests:
# Request 1: Creates Button(id="submit-btn")
# Request 2: Creates Button(id="submit-btn") → Warning: "Overwriting..."
The Solution: Request Scope¶
Use Registry.request_scope() to isolate components per request:
from pyjinhx import Registry
@app.get("/")
def index():
with Registry.request_scope():
# Components here are isolated to this request
button = Button(id="submit-btn", text="Submit")
return button.render()
# Registry automatically cleaned up
On entry, request_scope() also clears pending mutations, initializes the request-tier load cache, and optionally sets load_context and client_backend. On exit — even when an exception occurs — it restores the previous registry state, warns about unconsumed mutations (when reactive dev is enabled), clears mutations, and resets the request cache.
load_context and client_backend are optional — bare Registry.request_scope() is enough for instance isolation.
For application-wide coverage, pyjinhx ships no middleware of its own. Prefer setup(app, ...), which registers middleware that calls Registry.request_scope(client_backend=FastAPIClientBackend(request)) for you (see the canonical FastAPI snippet). To wire it by hand instead, open the scope yourself:
from pyjinhx import Registry, setup
from pyjinhx.integrations.fastapi import FastAPIClientBackend
setup(app) # recommended
# or:
with Registry.request_scope(client_backend=FastAPIClientBackend(request)):
...
Nested Scopes¶
Scopes can be nested—each creates its own isolated registry:
with Registry.request_scope():
outer = Button(id="outer", text="Outer")
with Registry.request_scope():
# "outer" is not visible here
inner = Button(id="inner", text="Inner")
# "inner" is not visible here, "outer" is restored
Common Patterns¶
Clearing the Registry¶
For testing or resetting state:
Checking Registration¶
# Get all registered instances
instances = Registry.get_instances()
# Check if a specific component exists (using the composite key)
key = Registry.make_key("Button", "submit-btn")
if key in instances:
button = instances[key]
Same ID, Different Types¶
Different component types can use the same id:
class Card(BaseComponent):
id: str
title: str
class Modal(BaseComponent):
id: str
title: str
# Both can use id="main" without collision
card = Card(id="main", title="Card Title")
modal = Modal(id="main", title="Modal Title")
# Both are in the registry
assert len(Registry.get_instances()) == 2
HTML IDs
While the registry allows same IDs across types, remember that HTML id attributes must be unique in the DOM. Use distinct IDs if both components render on the same page.
Class Registry vs Instance Registry¶
PyJinHx maintains two separate registries:
| Registry | Scope | Purpose |
|---|---|---|
| Class registry | Process-wide | Maps class names to types (e.g., "Button" → Button) |
| Instance registry | Context-local | Maps composite keys to instances (e.g., "Button_submit" → instance) |
The class registry enables the Renderer to instantiate components from PascalCase tags. The instance registry enables cross-referencing in templates.
Template context precedence¶
During render, registered instances are injected into the Jinja context by id so templates can reference them by registry key. A component cannot cross-reference a component of the same type and id that is currently being rendered — such a reference renders empty and logs a warning. Different component types that share the same id are tracked independently, so nesting an InnerChip(id="x") inside an OuterShell(id="x") renders both in full. Component field values from model_dump() take precedence when a field name collides with an instance id (registry injection uses setdefault). Watch for this with ReactiveComponent, whose id defaults to the kebab-cased class name: a Total reactive component with a total field defaults its id to "total", so the field would shadow the instance. (A plain BaseComponent defaults to pjx-<n> — not the kebab-class default that reactive components get.)