Skip to content

ReactPy

Overview

ReactPy is ReactPy's standalone ASGI application. It serves the HTML document, the client bundle, and the websocket through which components render.

from reactpy.executors.asgi import ReactPy

my_app = ReactPy(root_component)

Reference

ReactPy(root_component, **settings)

Instantiate ReactPy once, at module level, and hand the instance to any ASGI server:

uvicorn main:my_app

ReactPy serves the page at every route that is not claimed by its own static or websocket paths.

from reactpy.executors.asgi import ReactPy

Parameters

  • root_component: The component to mount. A component constructor — the @component function itself, not a call to it. This app is a single-page application: there is exactly one root.

  • html_head (optional): Extra content for the document's <head>.

    Must be built with html.head

    Pass html.head(...) — not a bare html.title(...) or a html.div(...). Anything else raises at request time:

    ValueError: Head element must be constructed with `html.head`.
    

    Verified: this surfaces as a 500 on the index route, not at import time, so a typo here only shows up once the server is running.

  • prepend_body (optional): Content rendered at the start of the <body>, before the mount point. Accepts a VdomDict built with html.*, or None to omit. Defaults to html.noscript("Enable JavaScript to view this site.").

    This is the one part of the tree that is genuinely server-rendered. See what ReactPy does and does not render.

  • html_lang (optional): Value for the document's lang attribute. Defaults to "en".

  • http_headers (optional): Extra response headers for the index document — a plain dict[str, str]. Use it for CSP, X-Frame-Options, caching, and similar.

  • pyscript_setup (optional): Load PyScript into the document head, for running component code in the browser. Defaults to False. ReactPyCsr sets this for you.

  • pyscript_options (optional): A PyScriptOptions configuring that behaviour.

  • **settings: Any key from ReactPyConfig. Despite appearances these are not scoped to this app instance — each one is written to the matching global in reactpy.config, so the last executor constructed wins process-wide. See Configuration.

Returns

An ASGI application.


Usage

A standalone app

1
my_app = ReactPy(counter)
1
# TODO

Serve it with uvicorn main:my_app. The name you assign matters, because it is what uvicorn module:name refers to.

Page metadata and headers

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
my_app = ReactPy(
    page,
    # Must be built with html.head(); any other tag raises ValueError.
    html_head=html.head(
        html.title("My ReactPy App"),
        html.meta({"name": "description", "content": "A ReactPy app"}),
    ),
    html_lang="fr",
    http_headers={"X-Frame-Options": "DENY"},
    # Rendered into the <body> before the mount point, on the server.
    prepend_body=html.header({"id": "nav"}, "Server-rendered navigation"),
)
1
# TODO

Verified against a running server: lang="fr", the custom <title>, the custom response header, and the prepend_body markup all appear in the initial HTML.

Configuration

Settings are ordinary keyword arguments, and they reach the browser-side client:

1
2
3
4
5
6
7
8
9
my_app = ReactPy(
    page,
    path_prefix="/apps/reactpy/",
    reconnect_interval=500,
    reconnect_max_interval=30000,
    reconnect_max_retries=50,
    async_rendering=True,
    debug=False,
)
1
# TODO
Setting Default Meaning
path_prefix /reactpy/ Base path ReactPy serves its own static assets and websocket from.
web_modules_dir temp dir Where built web modules are cached.
reconnect_interval 750 Milliseconds before the first reconnect attempt.
reconnect_max_interval 60000 Ceiling on the reconnect backoff.
reconnect_max_retries 150 Attempts before giving up.
reconnect_backoff_multiplier 1.25 Growth factor between attempts.
async_rendering True Allow renders to overlap.
debug False Server-side checks and richer error text — see the note below.
max_queue_size 1000 Bound on the pending events of each handler target — the layout keeps a queue per target (core/layout.py, _event_queues), not one per connection.
event_proxy_timeout 5.0 Seconds to wait for the browser to answer one live event read before EventProxyTimeout.
event_proxy_max_depth 10 How deep one resolution may expand nested objects before they come back as references.
event_proxy_max_timeouts 3 Consecutive unanswered round trips tolerated before live access is switched off for the connection.
tests_default_timeout 15.0 Default timeout for reactpy.testing. Not settable this way — see the note below.

Verified: setting path_prefix="/apps/reactpy/" moves asset serving to that prefix, and the reconnect values appear in the generated mount call. The three event_proxy_* settings were verified by constructing an executor with them and reading the matching reactpy.config options back.

They belong to a handler which reads the browser rather than to the client bootstrap: they bound the round trips behind await event.target.value. Reach for them only when a handler is timing out or a deeply nested read comes back as references — see Protocol Structure.

These keywords are process-wide, not per-app

executors/utils.py, process_settings walks the keywords and calls set_current() on the matching reactpy.config Option object. Those objects are module-level globals, so the last constructor to run wins for the whole process — including for any app built before it. Verified: two ReactPy instances created with reconnect_interval=111 and reconnect_interval=999 leave REACTPY_RECONNECT_INTERVAL.current at 999 for both. There is no per-app configuration layer; do not host two apps that need different values.

tests_default_timeout is worse than ineffective. Its Option is declared mutable=False in reactpy/config.py, and _option.py, set_current raises TypeError on an immutable option — so passing it raises Option(REACTPY_TESTS_DEFAULT_TIMEOUT=15.0) cannot be modified after initial load out of the constructor. Set it via the REACTPY_TESTS_DEFAULT_TIMEOUT environment variable before import, or not at all.

debug is not a client switch. REACTPY_DEBUG turns on server-side VDOM and JSON-attribute validation, populates the model's error text (without it, a failed component ships "error": "" and renders as nothing), and changes a few asset URLs to their ?dev forms. See Common Props.

Each setting also has an environment-variable twin in reactpy.config — REACTPY_PATH_PREFIX, REACTPY_RECONNECT_INTERVAL, and so on. Since the keyword already writes to that same global option, the two differ only in timing: the environment variable is read when the module is first imported, the keyword whenever an executor is constructed.


What ReactPy does and does not render

This is the distinction that trips people up, because the executor has historically been described as "SSR".

The initial HTML document contains an empty mount point:

<body>
  <noscript>Enable JavaScript to view this site.</noscript>
  <div id="app" class=""></div>
  <script type="module" crossorigin="anonymous">
    import { mountReactPy } from "/reactpy/static/index.js";
    mountReactPy({ mountElement: document.getElementById("app"), ... });
  </script>
</body>

Verified by fetching the index route: your root component's markup is not in the response. The browser connects over the websocket and ReactPy renders and streams from there.

What is server-rendered into the first response:

  • prepend_body, converted with reactpy_to_string.
  • html_head.
  • The document shell: lang, static asset paths, the mount script.

Practical consequences:

  • Search engines and curl see an empty app. For content that must exist without JavaScript, render it yourself — reactpy_to_string turns a component into an HTML string, which you can place in prepend_body or emit from your own route. It renders the component's first render directly, without a layout, so the component cannot use any hook: use_state, use_ref, use_effect, use_location and the rest all raise RuntimeError: No life cycle hook is active. Are you rendering in a layout?. Pass plain html.* elements, or a component that calls none of them.
  • There is no first-paint flash to optimise away, because there is no first paint to replace.
  • A component that fails on the server fails after the page loads, leaving a blank <div id="app">. When the page loads but shows nothing, check the server log before the browser console.

Not the same as ReactPyCsr

ReactPyCsr is the one that runs component code in the browser via PyScript. ReactPy runs components on the server and sends VDOM over a websocket. Both need a server; they differ in where your Python executes. See ReactPyMiddleware for mounting into an existing ASGI app.

Authors: Archmonger
Reactive Python and affiliates.