Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions reflex/.templates/web/app/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { route } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";

export default [
route("404", "routes/[404]_._index.jsx", { id: "404" }),
route("404", "routes/[404]._index.jsx", { id: "404" }),
...(await flatRoutes({
ignoredRouteFiles: ["routes/\\[404\\]_._index.jsx"],
ignoredRouteFiles: ["routes/\\[404\\]._index.jsx"],
})),
route("*", "routes/[404]_._index.jsx"),
route("*", "routes/[404]._index.jsx"),
Comment thread
adhami3310 marked this conversation as resolved.
];
76 changes: 34 additions & 42 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,9 +765,9 @@ def add_page(
msg = "Route must be set if component is not a callable."
raise exceptions.RouteValueError(msg)
# Format the route.
route = format.format_route(component.__name__)
route = format.format_route(format.to_kebab_case(component.__name__))
else:
route = format.format_route(route, format_case=False)
route = format.format_route(route)

if route == constants.Page404.SLUG:
if component is None:
Expand Down Expand Up @@ -822,10 +822,11 @@ def add_page(
state = self._state if self._state else State
state.setup_dynamic_args(get_route_args(route))

if on_load:
self._load_events[route] = (
on_load if isinstance(on_load, list) else [on_load]
)
self._load_events[route] = (
(on_load if isinstance(on_load, list) else [on_load])
if on_load is not None
else []
)

self._unevaluated_pages[route] = unevaluated_page

Expand Down Expand Up @@ -854,48 +855,35 @@ def _compile_page(self, route: str, save_page: bool = True):
if save_page:
self._pages[route] = component

def get_load_events(self, route: str) -> list[IndividualEventType[()]]:
@functools.cached_property
def router(self) -> Callable[[str], str | None]:
"""Get the route computer function.

Returns:
The route computer function.
"""
from reflex.route import get_router

return get_router(list(self._pages))

def get_load_events(self, path: str) -> list[IndividualEventType[()]]:
"""Get the load events for a route.

Args:
route: The route to get the load events for.
path: The route to get the load events for.

Returns:
The load events for the route.
"""
route = route.lstrip("/").rstrip("/")
if route == "":
return self._load_events.get(constants.PageNames.INDEX_ROUTE, [])

# Separate the pages by route type.
static_page_paths_to_page_route = {}
dynamic_page_paths_to_page_route = {}
for page_route in list(self._pages) + list(self._unevaluated_pages):
page_path = page_route.lstrip("/").rstrip("/")
if "[" in page_path and "]" in page_path:
dynamic_page_paths_to_page_route[page_path] = page_route
else:
static_page_paths_to_page_route[page_path] = page_route

# Check for static routes.
if (page_route := static_page_paths_to_page_route.get(route)) is not None:
return self._load_events.get(page_route, [])

# Check for dynamic routes.
parts = route.split("/")
for page_path, page_route in dynamic_page_paths_to_page_route.items():
page_parts = page_path.split("/")
if len(page_parts) != len(parts):
continue
if all(
part == page_part
or (page_part.startswith("[") and page_part.endswith("]"))
for part, page_part in zip(parts, page_parts, strict=False)
):
return self._load_events.get(page_route, [])

# Default to 404 page load events if no match found.
return self._load_events.get("404", [])
four_oh_four_load_events = self._load_events.get("404", [])
route = self.router(path)
if not route:
# If the path is not a valid route, return the 404 page load events.
return four_oh_four_load_events
return self._load_events.get(
route,
four_oh_four_load_events,
)

def _check_routes_conflict(self, new_route: str):
"""Verify if there is any conflict between the new route and any existing route.
Expand All @@ -916,7 +904,6 @@ def _check_routes_conflict(self, new_route: str):
segments = (
constants.RouteRegex.SINGLE_SEGMENT,
constants.RouteRegex.DOUBLE_SEGMENT,
constants.RouteRegex.SINGLE_CATCHALL_SEGMENT,
constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT,
)
for route in self._pages:
Expand Down Expand Up @@ -1742,6 +1729,11 @@ async def process(
# assignment will recurse into substates and force recalculation of
# dependent ComputedVar (dynamic route variables)
state.router_data = router_data
router_data[constants.RouteVar.PATH] = "/" + (
app.router(path) or "404"
if (path := router_data.get(constants.RouteVar.PATH))
else "404"
).removeprefix("/")
state.router = RouterData(router_data)

# Preprocess the event.
Expand Down
6 changes: 4 additions & 2 deletions reflex/compiler/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,9 +417,11 @@ def _format_route_part(part: str) -> str:
if part.startswith("[") and part.endswith("]"):
if part.startswith(("[...", "[[...")):
return "$"
if part.startswith("[["):
return "($" + part.removeprefix("[[").removesuffix("]]") + ")"
# We don't add [] here since we are reusing them from the input
return "$" + part + "_"
return "[" + part + "]_"
return "$" + part
return "[" + part + "]"


def _path_to_file_stem(path: str) -> str:
Expand Down
19 changes: 13 additions & 6 deletions reflex/constants/route.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,30 @@ class RouteRegex(SimpleNamespace):
_CLOSING_BRACKET = r"\]"
_ARG_NAME = r"[a-zA-Z_]\w*"

# The regex for a valid arg name, e.g. "slug" in "[slug]"
_ARG_NAME_PATTERN = re.compile(_ARG_NAME)

SLUG = re.compile(r"[a-zA-Z0-9_-]+")
# match a single arg (i.e. "[slug]"), returns the name of the arg
ARG = re.compile(rf"{_OPENING_BRACKET}({_ARG_NAME}){_CLOSING_BRACKET}")
# match a single catch-all arg (i.e. "[...slug]" or "[[...slug]]"), returns the name of the arg
CATCHALL = re.compile(
rf"({_OPENING_BRACKET}?{_OPENING_BRACKET}{_DOT_DOT_DOT}[^[{_CLOSING_BRACKET}]*{_CLOSING_BRACKET}?{_CLOSING_BRACKET})"
# match a single optional arg (i.e. "[[slug]]"), returns the name of the arg
OPTIONAL_ARG = re.compile(
rf"{_OPENING_BRACKET * 2}({_ARG_NAME}){_CLOSING_BRACKET * 2}"
)

# match a single non-optional catch-all arg (i.e. "[...slug]"), returns the name of the arg
STRICT_CATCHALL = re.compile(
rf"{_OPENING_BRACKET}{_DOT_DOT_DOT}({_ARG_NAME}){_CLOSING_BRACKET}"
)
# match a snigle optional catch-all arg (i.e. "[[...slug]]"), returns the name of the arg
OPT_CATCHALL = re.compile(

# match a single optional catch-all arg (i.e. "[[...slug]]"), returns the name of the arg
OPTIONAL_CATCHALL = re.compile(
rf"{_OPENING_BRACKET * 2}{_DOT_DOT_DOT}({_ARG_NAME}){_CLOSING_BRACKET * 2}"
)

SPLAT_CATCHALL = "[[...splat]]"
SINGLE_SEGMENT = "__SINGLE_SEGMENT__"
DOUBLE_SEGMENT = "__DOUBLE_SEGMENT__"
SINGLE_CATCHALL_SEGMENT = "__SINGLE_CATCHALL_SEGMENT__"
DOUBLE_CATCHALL_SEGMENT = "__DOUBLE_CATCHALL_SEGMENT__"


Expand Down
Loading