Skip to content

ReactPyMiddleware

Overview

ReactPyMiddleware adds ReactPy routes to an ASGI application you already have. Requests ReactPy does not recognise fall through to your app.

from reactpy.executors.asgi import ReactPyMiddleware

my_app = ReactPyMiddleware(existing_asgi_app, root_components=["main.component"])

Reference

ReactPyMiddleware(app, root_components, **settings)

from reactpy.executors.asgi import ReactPyMiddleware

Parameters

  • app: The ASGI application to serve when a request does not match a ReactPy route. Any ASGI app works — Starlette, FastAPI, or hand-written.

  • root_components: A list, set, or tuple of dotted paths to root components, e.g. ["main.counter"]. The path must be importable by Python's import system: module path, dot, attribute name. Resolution is module.rsplit(".", 1) followed by getattr, so it must name a module that is importable from the server's working directory.

  • **settings: Any key from ReactPyConfig. See Configuration. path_prefix is validated at construction and raises ValueError if malformed.

Returns

An ASGI application.


Usage

Wrapping an existing app

1
my_app = ReactPyMiddleware(starlette_app, root_components=["main.counter"])
1
# TODO

The wrapped app is untouched. Verified with Starlette: a plain /healthz route still returned its text response through the middleware.

What the middleware actually claims

ReactPyMiddleware.__call__ handles exactly four kinds of request and forwards everything else:

Path Handled by
websocket matching {path_prefix}{dotted_path}/ ReactPy render stream
{path_prefix}static/... ReactPy client bundle
{path_prefix}modules/... built web modules
user-registered routes your handlers
anything else your app

The middleware never serves your HTML

This is the part that surprises people. Registering a dotted path does not create a page. Requesting "/" falls through to your app, and there is no {path_prefix}{dotted_path}/ HTTP route either — that pattern is matched only when the ASGI scope type is "websocket". Verified: requesting /reactpy/main.counter/ over HTTP returns 404, while the websocket on the same path streams the model.

You produce the document yourself, by emitting an empty mount element and the client bootstrap that points at one registered component.

Serving a component from your own route

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
async def home(request):
    # The mount point plus the client bootstrap, for one registered component.
    mount = server_side_component_html(
        element_id="app",
        class_="",
        # Trailing slash required: the websocket route matches `<prefix><dotted_path>/`.
        component_path="main.counter/",
    )
    return HTMLResponse(f'<html><body><div id="wrapper">{mount}</div></body></html>')


starlette_app = Starlette(routes=[Route("/", home)])

my_app = ReactPyMiddleware(starlette_app, root_components=["main.counter"])
1
# TODO

server_side_component_html emits the <div id="app">, the import map fallback, and the mountReactPy(...) call. Verified in a real browser: the counter rendered into <div id="app"> with [ReactPy] Connected! in the console.

The trailing slash in component_path is required

Pass "main.counter/", not "main.counter". The client opens ws://host/reactpy/<component_path>?path=/, and the server matches the websocket path against

^{path_prefix}(?P<dotted_path>[a-zA-Z0-9_.]+)/$

which requires the trailing slash. Verified both ways: without it the handshake fails with Unexpected response code and the console logs [ReactPy] Failed to connect!, leaving an empty div; with it, the component renders.

Multiple components

Register several and choose per page. Each page mounts exactly one:

my_app = ReactPyMiddleware(
    starlette_app,
    root_components=["main.counter", "main.settings", "main.profile"],
)

Then emit a different component_path from each of your routes.

Unregistered paths are rejected

"Attempting to use an unregistered root component <path>." is raised when a websocket asks for a dotted path you never listed. Register every component you intend to mount.

Import errors

A dotted path that cannot be imported fails at construction, with a message naming the part that failed:

ImportError: ReactPy failed to import "main"
AttributeError: ReactPy failed to import "counter" from "main"

The first means the module was not found; the second means the module loaded but has no such attribute. ValueError is raised earlier still if the path has no dot at all.

Choosing an executor

Goal Use
Whole app is ReactPy, no existing server code ReactPy
Add components to an existing ASGI app ReactPyMiddleware
Run component code in the browser via PyScript ReactPyCsr

ReactPy is itself a subclass of ReactPyMiddleware that registers a single root and serves its own index document, which is why the standalone executor can do the HTML the middleware leaves to you.

Authors: Archmonger