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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Don't forget to remove deprecated code on each major release!

### Added

- The `navigate` component now accepts integers for `to`, allowing relative navigation in the browser's history stack (e.g. `navigate(-1)` to go back, `navigate(1)` to go forward).
- Support for ReactPy v2.x (beta). The initial URL is now sourced from the ReactPy executor (`use_connection().location`) instead of a JS-side `popstate` effect, removing a redundant network round-trip on first load.

### Changed
Expand Down
15 changes: 11 additions & 4 deletions src/js/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,19 @@ export function Navigate({
replace = false,
}: NavigateProps): null {
React.useEffect(() => {
if (replace) {
replaceState(to);
if (typeof to === "number") {
// Relative history navigation (e.g. go back / go forward).
// The resulting popstate event is picked up by the History
// component, so no explicit callback is needed here.
window.history.go(to);
} else {
pushState(to);
if (replace) {
replaceState(to);
} else {
pushState(to);
}
onNavigateCallback(createLocationObject());
}
onNavigateCallback(createLocationObject());
return () => {};
}, []);

Expand Down
2 changes: 1 addition & 1 deletion src/js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ export interface LinkProps {

export interface NavigateProps {
onNavigateCallback: (location: ReactPyLocation) => void;
to: string;
to: string | number;
replace?: boolean;
}
20 changes: 14 additions & 6 deletions src/reactpy_router/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,18 @@ def route(path: str, element: Any | None, *routes: Route) -> Route:
return Route(path, element, routes)


def navigate(to: str, replace: bool = False, key: Key | None = None) -> Component:
def navigate(to: str | int, replace: bool = False, key: Key | None = None) -> Component:
"""
Navigate to a specified URL.

This function changes the browser's current URL when it is rendered.

Args:
to: The target URL to navigate to.
to: The target URL to navigate to, or an integer indicating the relative
position in the browser's history stack (e.g., ``-1`` to go back,
``1`` to go forward). See `History.go <https://developer.mozilla.org/en-US/docs/Web/API/History/go>`_.
replace: If True, the current history entry will be replaced \
with the new URL. Defaults to False.
with the new URL. Ignored when ``to`` is an integer. Defaults to False.

Returns:
The component responsible for navigation.
Expand All @@ -101,15 +103,21 @@ def navigate(to: str, replace: bool = False, key: Key | None = None) -> Componen


@component
def _navigate(to: str, replace: bool = False) -> VdomDict | None:
def _navigate(to: str | int, replace: bool = False) -> VdomDict | None:
location = use_connection().location
set_location = _use_route_state().set_location
new_path = to.split("?", 1)[0]

def on_navigate_callback(_event: dict[str, Any]) -> None:
set_location(Location(**_event))

if location.path != new_path:
if isinstance(to, int):
# Integer navigation (go back/forward) — always delegate to JS;
# the resulting popstate event is handled by the History component.
return Navigate({"onNavigateCallback": on_navigate_callback, "to": to, "replace": replace})

if isinstance(to, str):
new_path = to.split("?", 1)[0]
if location.path != new_path:
return Navigate({"onNavigateCallback": on_navigate_callback, "to": to, "replace": replace})

return None
107 changes: 107 additions & 0 deletions tests/test_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,3 +355,110 @@ def sample():
await display.page.wait_for_selector("#nav-a")
await display.page.go_back()
await display.page.wait_for_selector("#root-a")


async def test_navigate_component_go_back(display: DisplayFixture):
"""Navigate back using navigate(-1)."""

@component
def nav_btn():
nav, set_nav = use_state("")

if nav:
return navigate(nav)

return html.button(
{"onClick": lambda _: set_nav("/a"), "id": "nav-to-a"},
"Go to A",
)

@component
def back_btn():
delta, set_delta = use_state("")

if isinstance(delta, int):
return navigate(delta)

return html.button(
{"onClick": lambda _: set_delta(-1), "id": "go-back"},
"Go back",
)

@component
def sample():
return browser_router(
route("/", nav_btn()),
route("/a", back_btn()),
)

await display.show(sample)

# Navigate to /a
await display.page.wait_for_selector("#nav-to-a")
await display.page.click("#nav-to-a")

# Wait for the go-back button to appear on route /a
await display.page.wait_for_selector("#go-back")
assert await display.page.text_content("#go-back") == "Go back"

# Go back using navigate(-1)
await display.page.click("#go-back")

# Verify we're back at the root route
await display.page.wait_for_selector("#nav-to-a")
assert await display.page.text_content("#nav-to-a") == "Go to A"


async def test_navigate_component_go_forward(display: DisplayFixture):
"""Navigate forward using navigate(1)."""

@component
def forward_btn():
delta, set_delta = use_state("")

if isinstance(delta, int):
return navigate(delta)

return html.button(
{"onClick": lambda _: set_delta(1), "id": "go-forward"},
"Go forward",
)

@component
def back_btn():
delta, set_delta = use_state("")

if isinstance(delta, int):
return navigate(delta)

return html.button(
{"onClick": lambda _: set_delta(-1), "id": "go-back"},
"Go back",
)

@component
def sample():
return browser_router(
route("/", forward_btn()),
route("/a", back_btn()),
)

await display.show(sample)

# Navigate to /a via full page load (builds forward history entry)
await display.goto("/a")

# Go back to / via navigate(-1)
await display.page.wait_for_selector("#go-back")
await display.page.click("#go-back")

# Should now be at / with the "Go forward" button
await display.page.wait_for_selector("#go-forward")
assert await display.page.text_content("#go-forward") == "Go forward"

# Go forward to /a via navigate(1)
await display.page.click("#go-forward")

# Verify we're back at /a
await display.page.wait_for_selector("#go-back")
assert await display.page.text_content("#go-back") == "Go back"
Loading