@tschk/moonshine-react-router 0.4.0 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -65,12 +65,11 @@ provided — do not expect them to work by aliasing:
65
65
  `<OutletProvider>` rather than derived from nested config.
66
66
  - **Progressive enhancement**: `<Form>`, `<Scripts>`, `<Meta>`, `<Links>`,
67
67
  `<ScrollRestoration>`, `<Await>`, `useFormAction`.
68
- - **History state**: `location.state`, `location.key` stability and
69
- `useNavigationType` distinguishing PUSH/POP/REPLACE. `state` is always `null`
70
- and `useNavigationType()` always returns `"POP"`.
68
+ - **History keys**: `location.state` and PUSH/POP/REPLACE navigation types are
69
+ supported, but `location.key` is URL-derived rather than unique per entry.
71
70
  - **Error boundaries**: `useRouteError`, `errorElement`. `ErrorResponse` and
72
71
  `isRouteErrorResponse` exist as value types only.
73
- - `useSearchParams` reads `window.location.search`; on the server it is empty
74
- because moonshine's router signal carries the pathname only.
72
+ - `useSearchParams` reads the owning router's pathname-plus-query signal,
73
+ including its scoped server snapshot.
75
74
 
76
75
  See [Moonshine README](../../README.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tschk/moonshine-react-router",
3
- "version": "0.4.0",
3
+ "version": "0.4.4",
4
4
  "description": "react-router / Remix client API reimplemented on moonshine — drop react-router, keep the imports",
5
5
  "license": "ISC",
6
6
  "type": "module",
@@ -17,8 +17,8 @@
17
17
  "./aliases": "./src/aliases.ts"
18
18
  },
19
19
  "dependencies": {
20
- "@tschk/moonshine": "^0.3.6",
21
- "@tschk/moonshine-router": "^0.3.6"
20
+ "@tschk/moonshine": "^0.4.4",
21
+ "@tschk/moonshine-router": "^0.4.4"
22
22
  },
23
23
  "peerDependencies": {
24
24
  "react": ">=18",
@@ -39,5 +39,13 @@
39
39
  "lint": "bunx oxlint .",
40
40
  "typecheck": "bunx tsc --noEmit",
41
41
  "test": "bun test"
42
+ },
43
+ "peerDependenciesMeta": {
44
+ "react": {
45
+ "optional": true
46
+ },
47
+ "react-dom": {
48
+ "optional": true
49
+ }
42
50
  }
43
51
  }
package/src/link.ts CHANGED
@@ -17,7 +17,10 @@ import {
17
17
  type MouseEvent,
18
18
  type ReactNode,
19
19
  } from "react";
20
- import { isExternal, navigate } from "@tschk/moonshine/router";
20
+ import {
21
+ isExternal,
22
+ useNavigate as useMoonshineNavigate,
23
+ } from "@tschk/moonshine/router";
21
24
  import { toHref, type To } from "./location";
22
25
  import { useLocation } from "./navigation";
23
26
 
@@ -28,7 +31,6 @@ export type LinkProps = Omit<
28
31
  to: To;
29
32
  replace?: boolean;
30
33
  preventScrollReset?: boolean;
31
- /** Accepted for source compatibility; history state is not tracked. */
32
34
  state?: unknown;
33
35
  reloadDocument?: boolean;
34
36
  /** `"intent"` warms the URL on hover, `"render"` on mount. */
@@ -51,7 +53,7 @@ export function Link({
51
53
  to,
52
54
  replace,
53
55
  preventScrollReset,
54
- state: _state,
56
+ state,
55
57
  reloadDocument,
56
58
  prefetch = "intent",
57
59
  children,
@@ -59,8 +61,9 @@ export function Link({
59
61
  onMouseEnter,
60
62
  ...rest
61
63
  }: LinkProps): ReactNode {
64
+ const navigate = useMoonshineNavigate();
62
65
  const href = toHref(to);
63
- const external = isExternal(href, rest.target);
66
+ const external = isExternal(href, rest.target, rest.download);
64
67
  const ref = useRef<HTMLAnchorElement>(null);
65
68
  // Navigation runs in a transition so a slow subtree cannot block the click.
66
69
  const [, startTransition] = useTransition();
@@ -122,7 +125,7 @@ export function Link({
122
125
  }
123
126
  event.preventDefault();
124
127
  startTransition(() => {
125
- navigate(href, { replace });
128
+ navigate(href, { replace, state });
126
129
  if (preventScrollReset !== true) globalThis.scrollTo?.(0, 0);
127
130
  });
128
131
  },
@@ -190,11 +193,12 @@ export type NavigateProps = {
190
193
  };
191
194
 
192
195
  /** Redirects on mount, the declarative form of `useNavigate()`. */
193
- export function Navigate({ to, replace }: NavigateProps): null {
196
+ export function Navigate({ to, replace, state }: NavigateProps): null {
197
+ const navigate = useMoonshineNavigate();
194
198
  const href = toHref(to);
195
199
  useEffect(() => {
196
- navigate(href, { replace });
197
- }, [href, replace]);
200
+ navigate(href, { replace, state });
201
+ }, [navigate, href, replace, state]);
198
202
  return null;
199
203
  }
200
204
 
package/src/location.ts CHANGED
@@ -3,11 +3,13 @@
3
3
  /**
4
4
  * Location shape shared by the react-router hooks.
5
5
  *
6
- * Moonshine's router signal carries the pathname only, so `search` and `hash`
7
- * are read back from `window.location` on each render.
6
+ * History state is subscribed independently so same-path navigations update.
8
7
  */
9
8
  import { useMemo, useReducer } from "react";
10
- import { useLocation as useMoonshineLocation } from "@tschk/moonshine/router";
9
+ import {
10
+ useLocation as useMoonshineLocation,
11
+ useHistory as useMoonshineHistory,
12
+ } from "@tschk/moonshine/router";
11
13
 
12
14
  export type Location<S = unknown> = {
13
15
  pathname: string;
@@ -35,7 +37,7 @@ export function toHref(to: To): string {
35
37
  return `${to.pathname ?? ""}${search}${hash}`;
36
38
  }
37
39
 
38
- function browserPart(part: "search" | "hash"): string {
40
+ function browserPart(part: "hash"): string {
39
41
  if (typeof window === "undefined") return "";
40
42
  return window.location[part];
41
43
  }
@@ -47,18 +49,19 @@ export function useRerender(): () => void {
47
49
  }
48
50
 
49
51
  export function useLocationObject(): Location {
50
- const pathname = useMoonshineLocation();
51
- const search = browserPart("search");
52
+ const source = useMoonshineLocation();
53
+ const pathname = source.split("?", 1)[0]!;
54
+ const history = useMoonshineHistory();
55
+ const search = source.includes("?") ? source.slice(source.indexOf("?")) : "";
52
56
  const hash = browserPart("hash");
53
57
  return useMemo(
54
58
  () => ({
55
59
  pathname,
56
60
  search,
57
61
  hash,
58
- // History state is not tracked by the moonshine router.
59
- state: null,
62
+ state: history.state,
60
63
  key: `${pathname}${search}${hash}`,
61
64
  }),
62
- [pathname, search, hash],
65
+ [pathname, search, hash, history],
63
66
  );
64
67
  }
package/src/navigation.ts CHANGED
@@ -9,7 +9,8 @@
9
9
  */
10
10
  import { useCallback, useMemo } from "react";
11
11
  import {
12
- navigate,
12
+ useNavigate as useMoonshineNavigate,
13
+ useHistory as useMoonshineHistory,
13
14
  useParams as useMoonshineParams,
14
15
  } from "@tschk/moonshine/router";
15
16
  import {
@@ -22,7 +23,6 @@ import {
22
23
 
23
24
  export type NavigateOptions = {
24
25
  replace?: boolean;
25
- /** Accepted for source compatibility; history state is not tracked. */
26
26
  state?: unknown;
27
27
  preventScrollReset?: boolean;
28
28
  };
@@ -37,6 +37,7 @@ export function useLocation(): Location {
37
37
  }
38
38
 
39
39
  export function useNavigate(): NavigateFunction {
40
+ const navigate = useMoonshineNavigate();
40
41
  const rerender = useRerender();
41
42
  return useCallback(
42
43
  (to: To | number, options?: NavigateOptions) => {
@@ -44,11 +45,14 @@ export function useNavigate(): NavigateFunction {
44
45
  globalThis.history?.go(to);
45
46
  return;
46
47
  }
47
- navigate(toHref(to), { replace: options?.replace });
48
+ navigate(toHref(to), {
49
+ replace: options?.replace,
50
+ state: options?.state,
51
+ });
48
52
  if (options?.preventScrollReset !== true) globalThis.scrollTo?.(0, 0);
49
53
  rerender();
50
54
  },
51
- [rerender],
55
+ [navigate, rerender],
52
56
  ) as NavigateFunction;
53
57
  }
54
58
 
@@ -67,6 +71,7 @@ export type SetURLSearchParams = (
67
71
  ) => void;
68
72
 
69
73
  export function useSearchParams(): [URLSearchParams, SetURLSearchParams] {
74
+ const navigate = useMoonshineNavigate();
70
75
  const location = useLocationObject();
71
76
  const rerender = useRerender();
72
77
  const params = useMemo(
@@ -85,11 +90,11 @@ export function useSearchParams(): [URLSearchParams, SetURLSearchParams] {
85
90
  const query = resolved.toString();
86
91
  navigate(`${location.pathname}${query ? `?${query}` : ""}`, {
87
92
  replace: options?.replace,
93
+ state: options?.state,
88
94
  });
89
- // A query-only navigation leaves the pathname signal unchanged.
90
95
  rerender();
91
96
  },
92
- [location.pathname, location.search, rerender],
97
+ [navigate, location.pathname, location.search, rerender],
93
98
  );
94
99
 
95
100
  return [params, setSearchParams];
@@ -129,10 +134,7 @@ export function useHref(to: To): string {
129
134
 
130
135
  export type NavigationType = "POP" | "PUSH" | "REPLACE";
131
136
 
132
- /**
133
- * Moonshine does not record how the current entry was reached, so this always
134
- * reports `POP`.
135
- */
137
+ /** How the current history entry was reached. */
136
138
  export function useNavigationType(): NavigationType {
137
- return "POP";
139
+ return useMoonshineHistory().action;
138
140
  }