Jinja
Overview¶
ReactPyJinja adds three tags to a Jinja2 environment so an existing Python web app can mount ReactPy components inside templates it already owns — one tag for server-rendered components, two for components that run in the browser via PyScript.
{% component "my_app.pages.counter" %}
Install the extra the extension needs:
pip install "reactpy[asgi,jinja]"
It lives at reactpy.templatetags.ReactPyJinja. Despite the templatetags name it is not a Django app — it is a jinja2-simple-tags StandaloneTag, so it works with any Jinja2 environment.
Reference¶
Registering the extension¶
Two equivalent forms:
from jinja2 import Environment, FileSystemLoader
from reactpy.templatetags import ReactPyJinja
# at construction
env = Environment(loader=FileSystemLoader("templates"), extensions=[ReactPyJinja])
# or afterwards
env = Environment(loader=FileSystemLoader("templates"))
env.add_extension(ReactPyJinja)
Both register all three tags at once; there is no way to register one alone, and no configuration to pass. Registering ReactPyJinja(...) as an instance and calling .register() does not work — that object has no register method.
Register before a template is compiled
Tags are resolved when a template is parsed. If the environment has already compiled a template that uses them, you get jinja2.exceptions.TemplateSyntaxError: Encountered unknown tag 'component'. Frameworks that cache templates at startup make this a startup-time error, which is easy to attribute to something else.
{% component "dotted.path" %}¶
Emits the markup that mounts a server-side component: an empty mount element, an import-map fallback, and a module script that calls mountReactPy.
{% component "my_app.pages.counter" %}
{% component "my_app.pages.badge", class="badge" %}
- The dotted path (required): the import path of the root component function — the same string you give
ReactPyMiddleware'sroot_components. It is embedded verbatim into the emitted JavaScript; ReactPy imports it on the server when the browser asks to connect. class(optional): a CSS class for the generated mount element, producing<div id="..." class="badge"></div>.
safe_output is set on the tag, so Jinja does not escape the returned HTML even under autoescape=True.
Arguments are parsed as Python expressions
The tag uses Jinja's argument parser, so keyword arguments must follow a comma — {% component "m.f" class="x" %} fails at template-compile time with TemplateSyntaxError: expected token ',', got 'class'. Write {% component "m.f", class="x" %}.
The path may be a quoted string (as above) or a Jinja expression that resolves to one; in the latter case it is read from the template context, so an unquoted my_app.pages.counter requires my_app to be a context variable.
Errors this tag raises¶
| Mistake | Error |
|---|---|
| Omitting the path | TypeError: component() missing 1 required positional argument: 'dotted_path' |
| Passing a second positional | TypeError: component() takes 1 positional argument but 2 were given |
| Any other keyword | ValueError: Unexpected keyword arguments: id, style |
| A typo in the tag name | TemplateSyntaxError: Encountered unknown tag 'bogus_tag'. |
A wrong dotted path is not one of them. Nothing checks that the module or attribute exists: the tag happily emits markup pointing at a path that will never connect, and the only symptom is a permanently empty element plus a failed request in the browser console. Verify the path against what you passed to root_components.
{% pyscript_setup %}¶
Emits the PyScript bootstrap — stylesheet, module script, and the config that installs ReactPy into the browser.
<head>
{% pyscript_setup %}
</head>
extra_py(optional, positional, repeatable): extra Python packages for the PyScript runtime —{% pyscript_setup "numpy", "rich" %}. They are appended to the package list alongside ReactPy's own wheel.extra_js(optional, keyword): a JSON object mapping module URLs to names, e.g.extra_js='{"./my.js": "mylib"}'. It is merged into the config'sjs_modules.main. A plain string that is not valid JSON raisesjson.decoder.JSONDecodeError.config(optional, keyword): a JSON object (or JSON string) merged over the generated config, letting you set anything PyScript accepts.
Put this tag in the document head, once. A page with several pyscript_component tags needs one bootstrap, not one per component.
Packages install in the browser
Anything in extra_py must be installable under Pyodide — pure Python, or a wheel built for it. A package with C extensions that installs cleanly on the server will fail in the browser, and the failure appears only in the browser console.
{% pyscript_component "path/to/file.py" %}¶
Emits a <div> plus a <script type='py'> that runs the named file's component in the browser.
{% pyscript_component "components/counter.py" initial="Loading..." %}
- File paths (required, positional, repeatable): paths to Python files, resolved relative to the process's current working directory at template-render time. Several files may be passed; they are concatenated into one script.
initial(optional): markup rendered inside the mount element while PyScript boots.root(optional): the name of the root component function, default"root".
Because the files are read and inlined, the browser never fetches them and your app serves no route for them.
The file's source ships to the client
The whole file — code, comments, docstrings, every string literal — is embedded in the HTML. Never read a secret into one of these files at render time, and do not use this tag for code you would not publish. This is unrelated to debug mode: turning reactpy.config.REACTPY_DEBUG off hides nothing here.
| Mistake | Error |
|---|---|
| File does not exist (wrong cwd or path) | FileNotFoundError: [Errno 2] No such file or directory: 'components/missing.py' |
No root function in the file | ValueError: Could not find the root component function 'root' in your PyScript file(s). |
Both are raised at render time, so they surface as a 500 on the page — not as a broken widget.
Prefer ReactPyCsr unless you are embedding
For a page built from scratch, ReactPyCsr is an ASGI app with its own URL and handles the setup tag for you. These two tags exist for putting a component inside a template you do not control. See ReactPyCsr.
Usage¶
A server-rendered component in an existing page¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
1 | |
The component must live in an importable module, since the browser reaches it by dotted path — a component defined inside the view function is not reachable that way. The template's path and the root_components list have to agree exactly.
A client-side component¶
Setup in the head, the component where it belongs:
1 2 3 4 5 6 7 8 9 10 | |
1 2 3 4 5 6 7 8 9 10 | |
1 | |
There is no websocket, so use_connection, use_scope, and use_location have no connection to read and raise — see Use Connection.
Which one to use¶
{% component %} | {% pyscript_setup %} + {% pyscript_component %} | |
|---|---|---|
| Component runs | Server | Browser |
| Needs a websocket | Yes | No |
| Component source visible to users | No | Yes |
| First paint | Server HTML, then it connects | initial until PyScript boots |
| Can read the request | Yes (use_connection, use_scope, use_location) | No |
| Needs a route on your app | Yes | No |
| Packages must be Pyodide-compatible | No | Yes |
Related¶
- Add React to an Existing Project — the full Starlette wiring these tags are for
- ReactPyMiddleware — the middleware that serves server-rendered components, and the markup this tag generates
- ReactPyCsr — the ASGI-app form of the PyScript tags
- ReactPy — standalone server-rendered app
- Common Props — what a component may return