Skip to content

Commit 26655e5

Browse files
committed
Remove FirstLoad and handle URL mutations on client-side
1 parent a03b7d3 commit 26655e5

File tree

6 files changed

+32
-123
lines changed

6 files changed

+32
-123
lines changed

src/js/src/index.ts

Lines changed: 14 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import React from "preact/compat";
22
import ReactDOM from "preact/compat";
33
import { createLocationObject, pushState, replaceState } from "./utils";
4-
import {
5-
HistoryProps,
6-
LinkProps,
7-
NavigateProps,
8-
FirstLoadProps,
9-
} from "./types";
4+
import { HistoryProps, LinkProps, NavigateProps } from "./types";
105

116
/**
127
* Interface used to bind a ReactPy node to React.
@@ -26,8 +21,8 @@ export function bind(node) {
2621
* History component that captures browser "history go back" actions and notifies the server.
2722
*/
2823
export function History({ onHistoryChangeCallback }: HistoryProps): null {
24+
// Tell the server about history "popstate" events
2925
React.useEffect(() => {
30-
// Register a listener for the "popstate" event and send data back to the server using the `onHistoryChange` callback.
3126
const listener = () => {
3227
onHistoryChangeCallback(createLocationObject());
3328
};
@@ -40,16 +35,10 @@ export function History({ onHistoryChangeCallback }: HistoryProps): null {
4035
});
4136

4237
// Tell the server about the URL during the initial page load
43-
// FIXME: This code is commented out since it currently runs every time any component
44-
// is mounted due to a ReactPy core rendering bug. `FirstLoad` component is used instead.
45-
// https://github.com/reactive-python/reactpy/pull/1224
46-
// React.useEffect(() => {
47-
// onHistoryChange({
48-
// pathname: window.location.pathname,
49-
// search: window.location.search,
50-
// });
51-
// return () => {};
52-
// }, []);
38+
React.useEffect(() => {
39+
onHistoryChangeCallback(createLocationObject());
40+
return () => {};
41+
}, []);
5342
return null;
5443
}
5544

@@ -58,18 +47,18 @@ export function History({ onHistoryChangeCallback }: HistoryProps): null {
5847
*
5948
* This component is not the actual `<a>` link element. It is just an event
6049
* listener for ReactPy-Router's server-side link component.
61-
*
62-
* @disabled This component is currently unused due to a ReactPy core rendering bug
63-
* which causes duplicate rendering (and thus duplicate event listeners).
6450
*/
6551
export function Link({ onClickCallback, linkClass }: LinkProps): null {
6652
React.useEffect(() => {
6753
// Event function that will tell the server about clicks
68-
const handleClick = (event: MouseEvent) => {
69-
event.preventDefault();
70-
let to = (event.target as HTMLElement).getAttribute("href");
71-
pushState(to);
72-
onClickCallback(createLocationObject());
54+
const handleClick = (event: Event) => {
55+
let click_event = event as MouseEvent;
56+
if (!click_event.ctrlKey) {
57+
event.preventDefault();
58+
let to = (event.currentTarget as HTMLElement).getAttribute("href");
59+
pushState(to);
60+
onClickCallback(createLocationObject());
61+
}
7362
};
7463

7564
// Register the event listener
@@ -82,7 +71,6 @@ export function Link({ onClickCallback, linkClass }: LinkProps): null {
8271

8372
// Delete the event listener when the component is unmounted
8473
return () => {
85-
let link = document.querySelector(`.${linkClass}`);
8674
if (link) {
8775
link.removeEventListener("click", handleClick);
8876
}
@@ -111,19 +99,3 @@ export function Navigate({
11199

112100
return null;
113101
}
114-
115-
/**
116-
* FirstLoad component that captures the URL during the initial page load and notifies the server.
117-
*
118-
* FIXME: This component only exists because of a ReactPy core rendering bug, and should be removed when the bug
119-
* is fixed. In the future, all this logic should be handled by the `History` component.
120-
* https://github.com/reactive-python/reactpy/pull/1224
121-
*/
122-
export function FirstLoad({ onFirstLoadCallback }: FirstLoadProps): null {
123-
React.useEffect(() => {
124-
onFirstLoadCallback(createLocationObject());
125-
return () => {};
126-
}, []);
127-
128-
return null;
129-
}

src/js/src/types.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,3 @@ export interface NavigateProps {
1717
to: string;
1818
replace?: boolean;
1919
}
20-
21-
export interface FirstLoadProps {
22-
onFirstLoadCallback: (location: ReactPyLocation) => void;
23-
}

src/js/src/utils.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,18 @@ export function createLocationObject(): ReactPyLocation {
77
};
88
}
99

10-
export function pushState(to: string): void {
10+
export function pushState(to: any): void {
11+
if (typeof to !== "string") {
12+
console.error("pushState() requires a string argument.");
13+
return;
14+
}
1115
window.history.pushState(null, "", new URL(to, window.location.href));
1216
}
1317

14-
export function replaceState(to: string): void {
18+
export function replaceState(to: any): void {
19+
if (typeof to !== "string") {
20+
console.error("replaceState() requires a string argument.");
21+
return;
22+
}
1523
window.history.replaceState(null, "", new URL(to, window.location.href));
1624
}

src/reactpy_router/components.py

Lines changed: 5 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22

33
from pathlib import Path
44
from typing import TYPE_CHECKING, Any
5-
from urllib.parse import urljoin
65
from uuid import uuid4
76

8-
from reactpy import component, html, use_connection
7+
from reactpy import component, html, use_connection, use_ref
98
from reactpy.backend.types import Location
109
from reactpy.web.module import export, module_from_file
1110

@@ -34,13 +33,6 @@
3433
)
3534
"""Client-side portion of the navigate component"""
3635

37-
FirstLoad = export(
38-
module_from_file("reactpy-router", file=Path(__file__).parent / "static" / "bundle.js"),
39-
("FirstLoad"),
40-
)
41-
42-
link_js_content = (Path(__file__).parent / "static" / "link.js").read_text(encoding="utf-8")
43-
4436

4537
def link(attributes: dict[str, Any], *children: Any, key: Key | None = None) -> Component:
4638
"""
@@ -59,8 +51,7 @@ def link(attributes: dict[str, Any], *children: Any, key: Key | None = None) ->
5951
@component
6052
def _link(attributes: dict[str, Any], *children: Any) -> VdomDict:
6153
attributes = attributes.copy()
62-
uuid_string = f"link-{uuid4().hex}"
63-
class_name = f"{uuid_string}"
54+
class_name = use_ref(f"link-{uuid4().hex}").current
6455
set_location = _use_route_state().set_location
6556
if "className" in attributes:
6657
class_name = " ".join([attributes.pop("className"), class_name])
@@ -80,44 +71,10 @@ def _link(attributes: dict[str, Any], *children: Any) -> VdomDict:
8071
"className": class_name,
8172
}
8273

83-
# FIXME: This component currently works in a "dumb" way by trusting that ReactPy's script tag \
84-
# properly sets the location due to bugs in ReactPy rendering.
85-
# https://github.com/reactive-python/reactpy/pull/1224
86-
current_path = use_connection().location.pathname
87-
88-
def on_click(_event: dict[str, Any]) -> None:
89-
if _event.get("ctrlKey", False):
90-
return
91-
92-
pathname, search = to.split("?", 1) if "?" in to else (to, "")
93-
if search:
94-
search = f"?{search}"
95-
96-
# Resolve relative paths that match `../foo`
97-
if pathname.startswith("../"):
98-
pathname = urljoin(current_path, pathname)
99-
100-
# Resolve relative paths that match `foo`
101-
if not pathname.startswith("/"):
102-
pathname = urljoin(current_path, pathname)
103-
104-
# Resolve relative paths that match `/foo/../bar`
105-
while "/../" in pathname:
106-
part_1, part_2 = pathname.split("/../", 1)
107-
pathname = urljoin(f"{part_1}/", f"../{part_2}")
108-
109-
# Resolve relative paths that match `foo/./bar`
110-
pathname = pathname.replace("/./", "/")
111-
112-
set_location(Location(pathname, search))
113-
114-
attrs["onClick"] = on_click
115-
116-
return html._(html.a(attrs, *children), html.script(link_js_content.replace("UUID", uuid_string)))
74+
def on_click_callback(_event: dict[str, Any]) -> None:
75+
set_location(Location(**_event))
11776

118-
# def on_click_callback(_event: dict[str, Any]) -> None:
119-
# set_location(Location(**_event))
120-
# return html._(html.a(attrs, *children), Link({"onClickCallback": on_click_callback, "linkClass": uuid_string}))
77+
return html._(Link({"onClickCallback": on_click_callback, "linkClass": class_name}), html.a(attrs, *children))
12178

12279

12380
def route(path: str, element: Any | None, *routes: Route) -> Route:

src/reactpy_router/routers.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from reactpy.core.hooks import ConnectionContext, use_connection
1212
from reactpy.types import ComponentType, VdomDict
1313

14-
from reactpy_router.components import FirstLoad, History
14+
from reactpy_router.components import History
1515
from reactpy_router.hooks import RouteState, _route_state_context
1616
from reactpy_router.resolvers import StarletteResolver
1717

@@ -76,9 +76,9 @@ def router(
7676
if match:
7777
if first_load:
7878
# We need skip rendering the application on 'first_load' to avoid
79-
# rendering it twice. The second render occurs following
80-
# the impending on_history_change event
79+
# rendering it twice. The second render follows the on_history_change event
8180
route_elements = []
81+
set_first_load(False)
8282
else:
8383
route_elements = [
8484
_route_state_context(
@@ -94,15 +94,8 @@ def on_history_change(event: dict[str, Any]) -> None:
9494
if location != new_location:
9595
set_location(new_location)
9696

97-
def on_first_load(event: dict[str, Any]) -> None:
98-
"""Callback function used within the JavaScript `FirstLoad` component."""
99-
if first_load:
100-
set_first_load(False)
101-
on_history_change(event)
102-
10397
return ConnectionContext(
10498
History({"onHistoryChangeCallback": on_history_change}), # type: ignore[return-value]
105-
FirstLoad({"onFirstLoadCallback": on_first_load}) if first_load else "",
10699
*route_elements,
107100
value=Connection(old_conn.scope, location, old_conn.carrier),
108101
)

src/reactpy_router/static/link.js

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)