You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 3f0ce61
Browse filesBrowse the repository at this point in the historyBrowse files
docs(reference): fix executor and client-api accuracy
- reactpy.md: executor **settings write global config Options via
process_settings/set_current, so the last constructor wins process-wide;
tests_default_timeout is mutable=False and raises TypeError; debug is
server-side validation + error text, not a client switch.
- reactpy-middleware.md: match_extra_paths returns None -- no user-registered
route row; the route registry belongs to standalone ReactPy.
- reactpy-csr.md: file_paths are functools.cache'd, not re-read per request;
the second package is jsonpointer==3.* and morphdom is a js_modules entry.
- client-api.md / manually-register-a-client.md: the NotImplementedError keeps
its %s placeholders and the path (it is raised as an unformatted tuple);
the 'internal bindings' line comes from bind.tsx, not the Python bootstrap;
the react probe runs only when a module lacks bind, which is optional.
- examples: import_map page used componentPath import_map.counter against
root_components main.counter -- the app could not mount (verified: server
raised 'unregistered root component'). Now main.counter/ and it works.
Copy file name to clipboardExpand all lines: docs/src/learn/manually-register-a-client.md
+12-2Lines changed: 12 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -46,7 +46,7 @@ Three details are load-bearing, all verified in a real browser:
46
46
-**`#!js componentPath` carries a trailing slash** and must name a component the server knows. `#!js "main.counter/"`, not `#!js "main.counter"`.
47
47
-**The import must be `#!html <script type="module">`.** The bundle statically imports two sibling chunks; a classic script fails with `#!python SyntaxError: Cannot use import statement outside a module` before anything ReactPy-related runs.
48
48
49
-
The component itself lives in a Python module the backend can import. Under `#!python ReactPyMiddleware`, `#!python componentPath` is the dotted path you listed in `#!python root_components` plus a slash — the middleware matches `#!python ^{path_prefix}(?P<dotted_path>[a-zA-Z0-9_.]+)/$`. Under `#!python ReactPy` there is one root and `#!python componentPath` must be **omitted**: the client builds the URL as `#!js pathPrefix + componentPath` (`static/index.js`) and `ReactPy.match_dispatch_path` compares the result against `#!python dispatcher_path` exactly, so a stray `#!js "/"` produces `#!python /reactpy//`, matches nothing, and is rejected with `#!python NotImplementedError: ReactPy app received unsupported request of type 'websocket'`. See [The executors route differently](../reference/client-api.md#the-executors-route-differently).
49
+
The component itself lives in a Python module the backend can import. Under `#!python ReactPyMiddleware`, `#!python componentPath` is the dotted path you listed in `#!python root_components` plus a slash — the middleware matches `#!python ^{path_prefix}(?P<dotted_path>[a-zA-Z0-9_.]+)/$`. Under `#!python ReactPy` there is one root and `#!python componentPath` must be **omitted**: the client builds the URL as `#!js pathPrefix + componentPath` (`static/index.js`) and `ReactPy.match_dispatch_path` compares the result against `#!python dispatcher_path` exactly, so a stray `#!js "/"` produces `#!python /reactpy//`, matches nothing, and the socket dies while the server logs a `NotImplementedError` — one whose `%s` placeholders were never filled in, so the line reads `("ReactPy app received unsupported request of type '%s' at path '%s'", 'websocket', '/reactpy//')`. Grep for `unsupported request of type`, not for the literal you expect. See [The executors route differently](../reference/client-api.md#the-executors-route-differently).
50
50
51
51
!!! warning "Nothing stops you from mounting twice into the same element"
52
52
@@ -126,7 +126,17 @@ app = ReactPy(
126
126
127
127
## Registering a JavaScript component
128
128
129
-
Python describes; the browser runs. A JavaScript component is any ES module that exports the names you ask for, and the module needs one extra export — a `#!python bind` function that tells the client how to create, render, and unmount nodes in a container the client owns:
129
+
Python describes; the browser runs. A JavaScript component is any ES module that exports the names you ask for. It *can* also export a `#!python bind` function that tells the client how to create, render, and unmount nodes in a container the client owns — and that is what you want, because the alternative is silent:
130
+
131
+
```js linenums="0"
132
+
// vdom.tsx, loadImportSource
133
+
let { bind } =module;
134
+
if (typeof bind !=="function") {
135
+
bind =awaitinfer_bind_from_environment();
136
+
}
137
+
```
138
+
139
+
`#!js bind` is therefore **optional**. Leave it out and the client infers one — first trying real React through the import map, then falling back to its bundled preact — and logs `#!js ReactPy will render JavaScript components using internal bindings for 'react'` at `console.debug`, with no `[ReactPy]` prefix. Your module still mounts and appears to work, so a missing or misnamed `#!js bind` is easy to miss. Ship it explicitly:
Copy file name to clipboardExpand all lines: docs/src/reference/client-api.md
+13-5Lines changed: 13 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -126,11 +126,19 @@ So the two want opposite things:
126
126
127
127
||`#!js componentPath`| Unmatched dotted path |
128
128
| --- | --- | --- |
129
-
|`#!python ReactPyMiddleware`|**required**, with trailing slash | Falls through to your app (commonly `403`) |
130
-
|`#!python ReactPy`|**must be omitted** — the bare `{pathPrefix}` is the route |`500`, from `NotImplementedError: ReactPy app received unsupported request of type 'websocket'`|
129
+
|`#!python ReactPyMiddleware`|**required**, with trailing slash | Falls through to your app (with Starlette's websocket handler, a `403` handshake rejection) |
130
+
|`#!python ReactPy`|**must be omitted** — the bare `{pathPrefix}` is the route |socket error; the server logs `NotImplementedError` (see below)|
131
131
132
132
Verified against both executors on the same build: the middleware accepts `#!js /reactpy/main.counter/` and refuses `#!js /reactpy/`; the standalone app does exactly the reverse. A mount snippet copied from one executor to the other therefore produces a socket that never opens.
133
133
134
+
The standalone rejection does not read like a normal exception. `executors/asgi/standalone.py`, `ReactPyApp.__call__` builds `msg` as a **tuple** of a `%`-format template and its arguments and raises `NotImplementedError(msg)` without ever formatting it, so the server logs it verbatim — the placeholders survive and the path is appended:
135
+
136
+
```text linenums="0"
137
+
NotImplementedError: ("ReactPy app received unsupported request of type '%s' at path '%s'", 'websocket', '/reactpy//')
138
+
```
139
+
140
+
Do not search the log for `...type 'websocket'`; search for `unsupported request of type`. The same tuple is written by `_logger.warning` immediately before the raise.
141
+
134
142
### The socket does not open immediately
135
143
136
144
Constructing the client does not connect. The websocket waits on a `#!js ready` promise, and that promise is resolved by the *first*`#!js onMessage` registration — nothing else in the codebase resolves it. The layout registers its handler from an effect, so the client connects only after that effect runs, which is why `#!js [ReactPy] Starting client...` always precedes `#!js Connected!` and never follows it.
@@ -150,7 +158,7 @@ Note that `#!js path` and `#!js qs` are query parameters, not path segments. The
150
158
151
159
For a server-rendered component, no import map is needed. The bundle imports its own two chunks by relative path and has preact inlined, so nothing it loads statically is a bare specifier. Verified: a page with no map at all mounted, connected, and the server never received a single request for `#!js preact.js`.
152
160
153
-
The map becomes load-bearing only when the model contains `#!json importSource`. Rendering such an element runs `#!js await import("react")` and `#!js await import("react-dom/client")` —*dynamic* imports, which are exactly what an import map resolves. If they succeed, real React renders the subtree; if they fail, the client falls back to its own preact bindings and says so:
161
+
The map becomes load-bearing only when the model contains `#!json importSource`**and** the resolved module does not export its own `#!js bind`. Rendering such an element goes through `#!js loadImportSource`, which uses the module's `#!js bind` when it is a function and only otherwise calls `#!js infer_bind_from_environment()` — the function that runs `#!js await import("react")` and `#!js await import("react-dom/client")`. Those are*dynamic* imports, which are exactly what an import map resolves. If they succeed, real React renders the subtree; if they fail, the client falls back to its own preact bindings and says so:
154
162
155
163
```text linenums="0"
156
164
ReactPy will render JavaScript components using internal bindings for 'react'.
@@ -172,7 +180,7 @@ The **id is the trigger, not the content**. Verified across three pages, all ser
172
180
| Your own map, any other id | Yes — a second map appears, alongside yours | The same `did not detect` line, plus the browser's own `An import map rule for specifier 'react' was removed, as it conflicted with an existing rule.`|
173
181
| Your own map with `id="reactpy-importmap"`| No | Nothing at all. |
174
182
175
-
Both of those messages come from the Python side of the bootstrap (`executors/utils.py`, `server_side_component_html`), emitted as unprefixed `#!js console.debug`, not from the client bundle.
183
+
The two messages have different authors, which matters when you search a minified bundle. The `#!js did not detect` line is written by the **Python** side of the bootstrap — `executors/utils.py`, `server_side_component_html` emits it inside the inline `#!html <script>` it generates, so it is in the served HTML, not in the bundle. The `#!js internal bindings for 'react'` line comes from the **client** — `#!js infer_bind_from_environment` in `bind.tsx`, reached from `loadImportSource` in `#!js vdom.tsx`. Both are unprefixed `#!js console.debug`, so neither shows up under a `[ReactPy]` filter.
176
184
177
185
To control resolution yourself, use that exact id. Two maps is worse than untidy: the browser keeps the **first** rule it sees for a specifier and discards the later one, so with two maps the winner is decided by document order — which is why the warning above names a rule being *removed*.
178
186
@@ -257,7 +265,7 @@ Nothing is required beyond an element and the call. Both files below were run to
257
265
# TODO
258
266
```
259
267
260
-
The component is registered with `#!python root_components`, and the `#!js componentPath` in the page — `#!python "hand_written.counter/"` — is that dotted path with a slash appended.
268
+
The component is registered with `#!python root_components`, and the `#!js componentPath` in the page — `#!python "main.counter/"` — is that dotted path with a slash appended.
261
269
262
270
!!! warning "The 404 in the console is usually not yours"
Copy file name to clipboardExpand all lines: docs/src/reference/reactpy-csr.md
+5-3Lines changed: 5 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -24,7 +24,9 @@ from reactpy.executors.asgi import ReactPyCsr
24
24
25
25
#### Parameters
26
26
27
-
-**`*file_paths`**: One or more paths to Python files containing components. Each is read at request time and its **source is inlined into the served HTML**. Accepts `#!python str` or `#!python pathlib.Path`.
27
+
-**`*file_paths`**: One or more paths to Python files containing components. Their **source is inlined into the served HTML**. Accepts `#!python str` or `#!python pathlib.Path`.
28
+
29
+
Read once, then cached for the life of the process: `executors/pyscript/utils.py`, `fetch_cached_python_file` is decorated with `#!python functools.cache`, keyed on the path. Editing the file does not change the page on reload — restart the server. The same cache strips comments and docstrings before inlining (see [Jinja](./jinja.md#pyscript_component-tag)), so what reaches the browser is not quite what you wrote.
28
30
29
31
-**`root_name`***(optional)*: The name of the root component inside your Python file. Defaults to `#!python "root"`. The file may define any number of components; this one is mounted.
30
32
@@ -89,10 +91,10 @@ Serve with `#!python uvicorn main:my_app`. The served page carries the `#!python
89
91
The page ReactPy generates does three things:
90
92
91
93
1. Links PyScript's CSS and module script from `{path_prefix}static/`.
92
-
2. Declares the packages to install, starting with ReactPy's own wheel and `#!python morphdom`.
94
+
2. Declares what PyScript should fetch: a `#!python packages` list for Python — ReactPy's own wheel from `{path_prefix}static/wheels/` and `#!python jsonpointer==3.*` — and a separate `#!python js_modules` entry for `#!python morphdom`, served from `{path_prefix}static/morphdom/morphdom-esm.js`. They are different lists with different installers; `#!python morphdom` is not a `#!python packages` entry.
93
95
3. Inlines your Python, then runs it under PyScript.
94
96
95
-
Verified against a running server: the initial response contains the PyScript `#!python core.js`, the `#!python packages` list pointing at the ReactPy wheel, and the `#!python initial` placeholder inside a `#!python <div class="pyscript">`.
97
+
Verified against a running server: the initial response carries the PyScript `#!python core.js`, `"packages": ["/reactpy/static/wheels/reactpy-2.0.0b13-py3-none-any.whl", "jsonpointer==3.*"]`, `"js_modules": {"main": {"/reactpy/static/morphdom/morphdom-esm.js": "morphdom"}}`, and the `#!python initial` placeholder inside a `#!python <div class="pyscript">`. `#!python pyscript_config` lets you replace either list.
There is no fourth row. `ReactPyMiddleware.match_extra_paths` is a hook that returns `#!python None` — its own comment says custom routes are "unused by default to encourage users to handle routing within your ASGI framework of choice". The `#!python @my_app.route(...)` registry people expect belongs to the standalone `#!python ReactPy` executor, which overrides that method to match `#!python extra_http_routes` / `#!python extra_ws_routes`. Under the middleware, every route is your app's.
69
+
69
70
!!! warning "The middleware never serves your HTML"
70
71
71
72
This is the part that surprises people. Registering a dotted path does **not** create a page. Requesting `#!python "/"` 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 `#!python "websocket"`. Verified: requesting `#!python /reactpy/main.counter/` over HTTP returns `404`, while the websocket on the same path streams the model.
Copy file name to clipboardExpand all lines: docs/src/reference/reactpy.md
+12-4Lines changed: 12 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -58,7 +58,7 @@ from reactpy.executors.asgi import ReactPy
58
58
59
59
-**`pyscript_options`***(optional)*: A `#!python PyScriptOptions` configuring that behaviour.
60
60
61
-
-**`**settings`**: Any key from `#!python ReactPyConfig`, applied to this app instance. See [Configuration](#configuration).
61
+
-**`**settings`**: Any key from `#!python ReactPyConfig`. Despite appearances these are **not** scoped to this app instance — each one is written to the matching global in `#!python reactpy.config`, so the last executor constructed wins process-wide. See [Configuration](#configuration).
62
62
63
63
#### Returns
64
64
@@ -125,13 +125,21 @@ Settings are ordinary keyword arguments, and they reach the browser-side client:
125
125
|`reconnect_max_retries`|`150`| Attempts before giving up. |
126
126
|`reconnect_backoff_multiplier`|`1.25`| Growth factor between attempts. |
127
127
|`async_rendering`|`True`| Allow renders to overlap. |
128
-
|`debug`|`False`|Extra client-side checks and messages. |
128
+
|`debug`|`False`|Server-side checks and richer error text — see the note below. |
129
129
|`max_queue_size`|`1000`| Bound on the pending event queue per connection. |
130
-
|`tests_default_timeout`|`15.0`| Default timeout for `reactpy.testing`. |
130
+
|`tests_default_timeout`|`15.0`| Default timeout for `reactpy.testing`. **Not settable this way** — see the note below. |
131
131
132
132
Verified: setting `#!python path_prefix="/apps/reactpy/"` moves asset serving to that prefix, and the reconnect values appear in the generated mount call.
133
133
134
-
Each has an environment-variable twin in `reactpy.config` — `#!python REACTPY_PATH_PREFIX`, `#!python REACTPY_RECONNECT_INTERVAL`, and so on. Passing the keyword is the per-app form; the environment variable is process-wide.
134
+
!!! warning "These keywords are process-wide, not per-app"
135
+
136
+
`executors/utils.py`, `process_settings` walks the keywords and calls `set_current()` on the matching `#!python 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 `#!python ReactPy` instances created with `#!python reconnect_interval=111` and `#!python reconnect_interval=999` leave `#!python REACTPY_RECONNECT_INTERVAL.current` at `#!python 999` for both. There is no per-app configuration layer; do not host two apps that need different values.
137
+
138
+
`#!python tests_default_timeout` is worse than ineffective. Its `Option` is declared `#!python mutable=False` in `reactpy/config.py`, and `#!python _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 `#!bash REACTPY_TESTS_DEFAULT_TIMEOUT` environment variable before import, or not at all.
139
+
140
+
`#!python debug` is not a client switch. `#!python REACTPY_DEBUG` turns on server-side VDOM and JSON-attribute validation, populates the model's `#!python error` text (without it, a failed component ships `#!python "error": ""` and renders as nothing), and changes a few asset URLs to their `#!js ?dev` forms. See [Common Props](common-props.md#invalid-dictionaries).
141
+
142
+
Each setting also has an environment-variable twin in `#!python reactpy.config` — `#!python REACTPY_PATH_PREFIX`, `#!python 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.
0 commit comments