Skip to content
17 changes: 9 additions & 8 deletions docs/pages/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,29 +207,30 @@ You can access the current page from the `router` attribute in any state. See th
```python
class State(rx.State):
def some_method(self):
current_page_route = self.router.page.path
current_page_url = self.router.page.raw_path
current_page_route = self.router.route_id
current_page_url = self.router.url.path
# ... Your logic here ...
```

The `router.page.path` attribute allows you to obtain the path of the current page from the router data,
The `router.route_id` attribute allows you to obtain the route pattern matched for the current page,
for [dynamic pages](/docs/pages/dynamic-routing) this will contain the slug rather than the actual value used to load the page.

To get the actual URL displayed in the browser, use `router.page.raw_path`. This
will contain all query parameters and dynamic path segments.
To get the actual URL path displayed in the browser, use `router.url.path`. For
query parameters and the URL fragment, use `router.url.query_parameters` and
`router.url.fragment` respectively.

In the above example, `current_page_route` will contain the route pattern (e.g., `/posts/[id]`), while `current_page_url`
will contain the actual URL (e.g., `/posts/123`).
will contain the actual URL path (e.g., `/posts/123`).

To get the full URL, access the same attributes with `full_` prefix.
To get the full URL (scheme, host, path, query and fragment), use `router.url` directly — it is a string subclass containing the complete URL.

Example:

```python
class State(rx.State):
@rx.var
def current_url(self) -> str:
return self.router.page.full_raw_path
return self.router.url


def index():
Expand Down
93 changes: 70 additions & 23 deletions docs/utility_methods/router_attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,18 @@ class RouterState(rx.State):


router_data = [
{"name": "rx.State.router.page.host", "value": RouterState.router.page.host},
{"name": "rx.State.router.page.path", "value": RouterState.router.page.path},
{"name": "rx.State.router.url", "value": RouterState.router.url},
{"name": "rx.State.router.url.scheme", "value": RouterState.router.url.scheme},
{"name": "rx.State.router.url.netloc", "value": RouterState.router.url.netloc},
{"name": "rx.State.router.url.origin", "value": RouterState.router.url.origin},
{"name": "rx.State.router.url.path", "value": RouterState.router.url.path},
{"name": "rx.State.router.url.query", "value": RouterState.router.url.query},
{
"name": "rx.State.router.page.raw_path",
"value": RouterState.router.page.raw_path,
},
{
"name": "rx.State.router.page.full_path",
"value": RouterState.router.page.full_path,
},
{
"name": "rx.State.router.page.full_raw_path",
"value": RouterState.router.page.full_raw_path,
},
{
"name": "rx.State.router.page.params",
"value": RouterState.router.page.params.to_string(),
"name": "rx.State.router.url.query_parameters",
"value": RouterState.router.url.query_parameters.to_string(),
},
{"name": "rx.State.router.url.fragment", "value": RouterState.router.url.fragment},
{"name": "rx.State.router.route_id", "value": RouterState.router.route_id},
{
"name": "rx.State.router.session.client_token",
"value": RouterState.router.session.client_token,
Expand Down Expand Up @@ -112,13 +106,8 @@ about the current page, session, or state.

The `self.router` attribute has several sub-attributes that provide various information:

- `router.page`: data about the current page and route
- `host`: The hostname and port serving the current page (frontend).
- `path`: The path of the current page (for dynamic pages, this will contain the slug)
- `raw_path`: The path of the page displayed in the browser (including params and dynamic values)
- `full_path`: `path` with `host` prefixed
- `full_raw_path`: `raw_path` with `host` prefixed
- `params`: Dictionary of query params associated with the request
- `router.url`: the URL of the current page, parsed into its components (see [URL Attributes](#url-attributes) below).
- `router.route_id`: the route pattern that matched the current request (e.g. `/posts/[id]`). For [dynamic pages](/docs/pages/dynamic_routing) this contains the slug rather than the actual value used to load the page.

- `router.session`: data about the current session
- `client_token`: UUID associated with the current tab's token. Each tab has a unique token.
Expand All @@ -141,6 +130,64 @@ The `self.router` attribute has several sub-attributes that provide various info
- `accept_language`: The accepted languages.
- `raw_headers`: A mapping of all HTTP headers as a frozen dictionary. This provides access to any header that was sent with the request, not just the common ones listed above.

## URL Attributes

`self.router.url` is the full URL of the page currently displayed in the browser, parsed into its components using Python's standard `urllib.parse.urlsplit`. It is a string subclass, so it can be used anywhere a string is expected (for example, passed to `rx.text(self.router.url)` to render the whole URL), and additionally exposes the following attributes:

- `scheme`: The URL scheme (e.g. `"http"` or `"https"`).
- `netloc`: The network location, including hostname and optional port (e.g. `"example.com:3000"`).
- `origin`: The scheme and netloc joined together (e.g. `"https://example.com:3000"`). Equivalent to `f"{scheme}://{netloc}"`.
- `path`: The URL path as displayed in the browser, including any filled-in dynamic segments but excluding the query string and fragment (e.g. `"/posts/123"`).
- `query`: The raw query string, without the leading `?` (e.g. `"tab=comments&sort=new"`).
- `query_parameters`: The query string parsed into a frozen, immutable `Mapping[str, str]`. Use this instead of parsing `query` by hand.
- `fragment`: The URL fragment, without the leading `#` (e.g. `"section-2"`). The client-side router sends the current fragment over the WebSocket, so this reflects whatever is shown in the browser URL bar.

### Example

For a request to `https://example.com:3000/posts/123?tab=comments#top` matching the route `/posts/[id]`:

| Attribute | Value |
| :----------------------------------- | :----------------------------------------------------- |
| `self.router.url` | `"https://example.com:3000/posts/123?tab=comments#top"`|
| `self.router.url.scheme` | `"https"` |
| `self.router.url.netloc` | `"example.com:3000"` |
| `self.router.url.origin` | `"https://example.com:3000"` |
| `self.router.url.path` | `"/posts/123"` |
| `self.router.url.query` | `"tab=comments"` |
| `self.router.url.query_parameters` | `{"tab": "comments"}` |
| `self.router.url.fragment` | `"top"` |
| `self.router.route_id` | `"/posts/[id]"` |

### Reading Query Parameters

`query_parameters` is the preferred way to read values from the query string. It is a frozen mapping (immutable and hashable), so it is safe to use inside `@rx.var` computed vars and event handlers:

```python
class State(rx.State):
@rx.var
def selected_tab(self) -> str:
return self.router.url.query_parameters.get("tab", "overview")

def on_load(self):
page = self.router.url.query_parameters.get("page", "1")
# ... load the appropriate data for that page ...
```

For dynamic path segments such as `[id]` or `[[...splat]]`, see [Dynamic Routes](/docs/pages/dynamic_routing) — those values are exposed as state vars on the root state (e.g. `rx.State.id`, `rx.State.splat`), not through `router.url`.

## Migrating from `router.page`

The `self.router.page` namespace is deprecated as of Reflex 0.8.1 and will be removed in 1.0. Its functionality is now provided by `self.router.url` together with `self.router.route_id`. Use the table below to update existing code:

| Deprecated | Replacement | Notes |
| :---------------------------------- | :------------------------------------------------------------ | :-------------------------------------------------------------------- |
| `self.router.page.path` | `self.router.route_id` | The route pattern, e.g. `/posts/[id]`. |
| `self.router.page.raw_path` | `self.router.url.path` | The actual path in the browser. Append `?{url.query}` if you also need query params. |
| `self.router.page.full_path` | `f"{self.router.url.origin}{self.router.route_id}"` | Origin prefixed onto the route pattern. Rarely needed. |
| `self.router.page.full_raw_path` | `self.router.url` | `router.url` is itself the full URL as a string. |
| `self.router.page.host` | `self.router.url.origin` | Full origin including scheme (e.g. `"http://localhost:3000"`). Use `self.router.url.netloc` for just `host:port`. |
| `self.router.page.params` | `self.router.url.query_parameters` | Now a frozen mapping rather than a plain dict. |

### Example Values on this Page

```python eval
Expand Down
196 changes: 194 additions & 2 deletions reflex/istate/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,22 @@
import dataclasses
from collections.abc import Mapping
from types import MappingProxyType
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any, ClassVar
from urllib.parse import _NetlocResultMixinStr, parse_qsl, urlsplit

from reflex_base import constants
from reflex_base.utils import console, format
from reflex_base.utils.serializers import serializer
from reflex_base.vars.base import (
CachedVarOperation,
Var,
VarData,
VarSubclassEntry,
_var_subclasses,
cached_property_no_lock,
)
from reflex_base.vars.object import ObjectItemOperation, ObjectVar
from reflex_base.vars.sequence import StringVar


@dataclasses.dataclass(frozen=True, init=False)
Expand Down Expand Up @@ -149,6 +159,184 @@ def __new__(cls, url: str):
return obj


@serializer(to=dict)
def _serialize_reflex_url(obj: ReflexURL) -> dict:
"""Serialize a ReflexURL to an object with its parsed components.

The full URL is exposed under the ``href`` key so the frontend can read
either the whole URL or any individual component without re-parsing.

Args:
obj: the ReflexURL to serialize.

Returns:
A dict with scheme, netloc, origin, path, query, query_parameters,
fragment, and href.
"""
return {
"scheme": obj.scheme,
"netloc": obj.netloc,
"origin": obj.origin,
"path": obj.path,
"query": obj.query,
"query_parameters": dict(obj.query_parameters),
"fragment": obj.fragment,
"href": str.__str__(obj),
}


class ReflexURLVar(StringVar[ReflexURL]):
"""Var type marker for ReflexURL.

Exists only to anchor the type registration for ``ReflexURL``; actual
instances returned by the Var system are always ``ReflexURLCastedVar``,
which exposes URL components as typed properties.
"""


@dataclasses.dataclass(
eq=False,
frozen=True,
slots=True,
)
class ReflexURLCastedVar(CachedVarOperation, ReflexURLVar):
"""Cast-to-ReflexURL operation whose default rendering reads ``href``.

Constructed when ``guess_type`` / ``to(ReflexURLVar)`` is invoked on any
Var typed as ReflexURL (e.g. ``State.router.url``). Its top-level JS
expression is ``{original}?.["href"]`` so string-context usage produces
the full URL; component access via the typed properties below reads the
matching key on ``{original}`` instead.
"""

_original: Var = dataclasses.field(
default_factory=lambda: Var(_js_expr="null", _var_type=None),
)
_default_var_type: ClassVar[Any] = ReflexURL

@cached_property_no_lock
def _cached_var_name(self) -> str:
"""Render the URL as its ``href`` string in JS.

Returns:
The JS expression for the full URL string.
"""
return f'{self._original!s}?.["href"]'

def _component(self, name: str) -> Var:
"""Build an indexing operation for a key on the serialized URL object.

The returned Var is untyped; each property wraps this in a
``.to(...)`` call to narrow it to the correct Var subclass.

Args:
name: The component key on the serialized URL object.

Returns:
A Var reading ``name`` from the URL object.
"""
return ObjectItemOperation.create(
self._original.to(ObjectVar, Mapping[str, Any]),
name,
)

@property
def scheme(self) -> StringVar:
"""The URL scheme (e.g. ``"https"``).

Returns:
StringVar accessing ``scheme`` on the serialized URL.
"""
return self._component("scheme").to(str)

@property
def netloc(self) -> StringVar:
"""The network location, including host and optional port.

Returns:
StringVar accessing ``netloc`` on the serialized URL.
"""
return self._component("netloc").to(str)

@property
def origin(self) -> StringVar:
"""The scheme and netloc joined (e.g. ``"https://example.com:3000"``).

Returns:
StringVar accessing ``origin`` on the serialized URL.
"""
return self._component("origin").to(str)

@property
def path(self) -> StringVar:
"""The URL path as shown in the browser (no query or fragment).

Returns:
StringVar accessing ``path`` on the serialized URL.
"""
return self._component("path").to(str)

@property
def query(self) -> StringVar:
"""The raw query string, without a leading ``?``.

Returns:
StringVar accessing ``query`` on the serialized URL.
"""
return self._component("query").to(str)

@property
def query_parameters(self) -> ObjectVar[Mapping[str, str]]:
"""The parsed query string as a mapping.

Returns:
ObjectVar accessing ``query_parameters`` on the serialized URL.
"""
return self._component("query_parameters").to(ObjectVar, Mapping[str, str])

@property
def fragment(self) -> StringVar:
"""The URL fragment, without a leading ``#``.

Returns:
StringVar accessing ``fragment`` on the serialized URL.
"""
return self._component("fragment").to(str)

@classmethod
def create(
cls,
value: Var,
_var_type: Any = None,
_var_data: VarData | None = None,
) -> "ReflexURLCastedVar":
"""Create a ReflexURLCastedVar wrapping another Var.

Args:
value: The Var being cast to ReflexURL.
_var_type: Optional override for the var type.
_var_data: Additional VarData to merge in.

Returns:
The new ReflexURLCastedVar.
"""
return cls(
_js_expr="",
_var_type=_var_type or ReflexURL,
_var_data=_var_data,
_original=value,
)


# ReflexURLCastedVar intentionally uses the CachedVarOperation lineage rather
# than ToOperation so _js_expr can render as {original}?.["href"]. The registry
# entry still accepts it because .to()/guess_type() only call .create(...),
# which has a compatible signature.
_var_subclasses.append(
VarSubclassEntry(ReflexURLVar, ReflexURLCastedVar, (ReflexURL,)) # pyright: ignore[reportArgumentType]
)


@dataclasses.dataclass(frozen=True)
class PageData:
"""An object containing page data."""
Expand Down Expand Up @@ -279,6 +467,10 @@ def serialize_router_data(obj: RouterData) -> dict:
"session": obj.session,
"headers": obj.headers,
"page": obj._page,
"url": obj.url,
# ReflexURL is a str subclass, so json.dumps handles it natively and
# never invokes the `default=serialize` hook. Call the URL serializer
# eagerly here so the frontend receives the parsed component dict
# instead of just the raw URL string.
"url": _serialize_reflex_url(obj.url),
"route_id": obj.route_id,
}
Loading
Loading