use-navigation-api 0.0.7 → 0.2.0

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/package.json CHANGED
@@ -21,12 +21,15 @@
21
21
  "require": "./dist/use-navigation-api.umd.js"
22
22
  }
23
23
  },
24
- "version": "0.0.7",
24
+ "version": "0.2.0",
25
25
  "type": "module",
26
26
  "scripts": {
27
27
  "build": "tsc -b && vite build",
28
28
  "lint": "eslint ."
29
29
  },
30
+ "dependencies": {
31
+ "uri-js": "^4.4.1"
32
+ },
30
33
  "peerDependencies": {
31
34
  "react": "^18.0.0 || ^19.0.0",
32
35
  "react-dom": "^18.0.0 || ^19.0.0"
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./navigationProvider";
2
- export * from "./location/useLocation.ts";
3
2
  export * from "./useNavigate";
4
- export * from "./location/useQueryParam.ts";
3
+ export * from "./location/useLocation";
4
+ export * from "./location/useQueryParam";
5
+ export * from "./location/useLocationWithParam";
@@ -1,37 +1,17 @@
1
1
  import { useContext, useMemo } from "react";
2
- import {
3
- NavigationContext,
4
- type NavigationContextState,
5
- } from "src/navigationProvider.ts";
2
+ import { NavigationContext } from "src/navigationProvider.ts";
3
+ import URI from "uri-js";
6
4
 
7
- type Location = { pathname: string; search: string; hash: string; url?: URL };
5
+ const DUMMY_BASE = "fakeprotodonotuse://";
8
6
 
9
- export function parseLocation(
10
- location: string,
11
- {
12
- url = window?.location?.href || "https://example.com/",
13
- }: NavigationContextState,
14
- ): Location {
15
- try {
16
- const parsed = new URL(location, url);
17
- return {
18
- url: parsed,
19
- pathname: parsed.pathname,
20
- search: parsed.search,
21
- hash: parsed.hash,
22
- };
23
- } catch {
24
- /* fall back to string parsing */
25
- }
26
- if (!location) return { pathname: "/", search: "", hash: "" };
27
- const pathname = location.split("?")[0];
28
- const search = location.substring(pathname.length).split("#")[0];
29
- const hash = location.substring(pathname.length + search.length);
30
- try {
31
- return { url: undefined, pathname, search, hash };
32
- } catch {
33
- return { url: undefined, pathname, search, hash };
34
- }
7
+ export type Location = string;
8
+
9
+ export function resolveLocation(location: string, base: string): string {
10
+ const parsed = URI.parse(URI.resolve(base, location));
11
+ parsed.scheme = undefined;
12
+ parsed.host = undefined;
13
+ parsed.port = undefined;
14
+ return URI.serialize(parsed);
35
15
  }
36
16
 
37
17
  /**
@@ -41,31 +21,29 @@ export function useLocation(): Location;
41
21
  /**
42
22
  * Resolves a URL string relative to the current location and returns its parts.
43
23
  */
44
- export function useLocation(url: string): Location;
45
- /**
46
- * Resolves a URL string relative to the current location after applying the supplied function.
47
- */
48
- export function useLocation<R>(
49
- url: string,
50
- parse: (location: string, value: NavigationContextState) => R,
51
- ): R;
24
+ export function useLocation(url: string, base?: string): Location;
52
25
  /**
53
26
  * Returns a location based on the returned url if present, or the modified input argument.
54
27
  * For instance, `useLocation(url => url.searchParams.set("parameter", "value"))` returns the current location with only a single parameter updated.
55
28
  */
56
- export function useLocation(url: (current: URL) => URL | void): Location;
29
+ export function useLocation(
30
+ url: (current: URL) => URL | void,
31
+ base?: string,
32
+ ): Location;
33
+
57
34
  export function useLocation(
58
35
  url?: string | ((current: URL) => URL | void),
59
- parse = parseLocation,
36
+ baseIn?: string,
60
37
  ) {
61
38
  const location = useContext(NavigationContext);
39
+ const base = baseIn ?? location.url;
62
40
  return useMemo(() => {
63
- if (typeof url === "string") return parse(url, location);
41
+ if (typeof url === "string") return resolveLocation(url, base);
64
42
  if (typeof url === "function") {
65
- const modified = new URL(location.url);
43
+ const modified = new URL(base, DUMMY_BASE);
66
44
  const returned = url(modified);
67
- return parse((returned || modified).href, location);
45
+ return resolveLocation((returned || modified).href, base);
68
46
  }
69
- return parse("", location);
70
- }, [parse, url, location]);
47
+ return resolveLocation("", base);
48
+ }, [url, base]);
71
49
  }
@@ -0,0 +1,45 @@
1
+ import { useLocation } from "src/location/useLocation.ts";
2
+
3
+ /**
4
+ * returns The current location with the specified parameter set to the provided value or removed if the value === null.
5
+ * @param param name of the query parameter to set
6
+ * @param value value to set the parameter to, or `null` to remove any existing value of that parameter
7
+ * @param base If provided, uses this value instead of the current location.
8
+ */
9
+ export function useLocationWithParam(
10
+ param: string,
11
+ value: string | null,
12
+ base?: string,
13
+ ) {
14
+ return useLocation(
15
+ (url) =>
16
+ value === null
17
+ ? url.searchParams.delete(param)
18
+ : url.searchParams.set(param, value),
19
+ base,
20
+ );
21
+ }
22
+
23
+ /**
24
+ * returns The current location with the specified parameter set to the provided value or removed if the value === null.
25
+ * @param params map of parameters to update. null or empty entries will be removed.
26
+ * @param base If provided, uses this value instead of the current location.
27
+ */
28
+ export function useLocationWithParams(
29
+ params: Record<string, string | string[] | null>,
30
+ base?: string,
31
+ ) {
32
+ return useLocation((url) => {
33
+ for (const [param, value] of Object.entries(params)) {
34
+ if (value === null) {
35
+ url.searchParams.delete(param);
36
+ } else if (Array.isArray(value)) {
37
+ url.searchParams.delete(param);
38
+ value.forEach((v) => url.searchParams.append(param, v));
39
+ } else {
40
+ url.searchParams.set(param, value);
41
+ }
42
+ }
43
+ return url;
44
+ }, base);
45
+ }
@@ -1,5 +1,10 @@
1
1
  import { useLocation } from "src/location/useLocation.ts";
2
2
  import { useMemo } from "react";
3
+ import URI from "uri-js";
4
+
5
+ export function parseSearchParams(location?: string) {
6
+ return new URLSearchParams(URI.parse(location || "").query);
7
+ }
3
8
 
4
9
  /**
5
10
  * Returns the first value for a query parameter, or null if it is missing.
@@ -9,12 +14,12 @@ export function useQueryParam(param: string, all?: false): string | null;
9
14
  * Returns all values for a query parameter when it appears multiple times.
10
15
  */
11
16
  export function useQueryParam(param: string, all: true): string[];
17
+
12
18
  export function useQueryParam(param: string, all: boolean = false) {
13
19
  const location = useLocation();
14
20
  return useMemo(() => {
15
21
  try {
16
- const searchParams =
17
- location.url?.searchParams || new URLSearchParams(location.search);
22
+ const searchParams = parseSearchParams(location);
18
23
  return all ? searchParams.getAll(param) || [] : searchParams.get(param);
19
24
  } catch {
20
25
  return null;
@@ -9,9 +9,9 @@ import {
9
9
  useMemo,
10
10
  useState,
11
11
  } from "react";
12
+ import URI from "uri-js";
12
13
 
13
14
  export type NavigationContextState = {
14
- navigation: Navigation;
15
15
  url: string;
16
16
  store: "url" | "hash" | "memory";
17
17
  };
@@ -19,8 +19,7 @@ type NavigationContextValue = NavigationContextState & {
19
19
  setState?: Dispatch<SetStateAction<NavigationContextState>>;
20
20
  };
21
21
  const defaultValue: NavigationContextValue = {
22
- navigation: window.navigation!,
23
- url: window?.location?.href || "/",
22
+ url: "/",
24
23
  store: "url" as "url" | "hash" | "memory",
25
24
  };
26
25
  export const NavigationContext = createContext(defaultValue);
@@ -41,9 +40,11 @@ export const NavigationProvider: FC<{
41
40
  scoped,
42
41
  shouldHandle = defaultShouldHandle,
43
42
  }) => {
44
- const navigation = defaultValue.navigation;
45
43
  const [scope, setScope] = useState<HTMLDivElement | null>(null);
46
- const [state, setState] = useState(() => ({ ...defaultValue, store }));
44
+ const [state, setState] = useState(() => ({
45
+ store,
46
+ url: window.location.href,
47
+ }));
47
48
  const skip = useMemo(
48
49
  () => (event: NavigateEvent) => {
49
50
  const target = event.sourceElement;
@@ -70,7 +71,7 @@ export const NavigationProvider: FC<{
70
71
  ) {
71
72
  const href = event.sourceElement.getAttribute("href");
72
73
  if (href && !href.startsWith("/") && !/^[a-z]+:\/\//i.test(href)) {
73
- url = new URL(href, prev.url).href;
74
+ url = URI.resolve(prev.url, href);
74
75
  }
75
76
  }
76
77
  return { ...prev, url, store };
@@ -78,7 +79,7 @@ export const NavigationProvider: FC<{
78
79
  } else {
79
80
  event.intercept({
80
81
  async handler() {
81
- setState({ navigation, url, store });
82
+ setState({ url, store });
82
83
  },
83
84
  });
84
85
  }
@@ -86,7 +87,7 @@ export const NavigationProvider: FC<{
86
87
 
87
88
  navigation?.addEventListener("navigate", handler);
88
89
  return () => navigation?.removeEventListener("navigate", handler);
89
- }, [skip, navigation, scoped, store]);
90
+ }, [skip, scoped, store]);
90
91
 
91
92
  const value = useMemo(() => ({ ...state, setState }), [state]);
92
93
 
@@ -1,11 +1,12 @@
1
1
  import { useContext, useMemo } from "react";
2
2
  import { NavigationContext } from "src/navigationProvider";
3
3
  import { future } from "src/util/future";
4
+ import URI from "uri-js";
4
5
 
5
6
  export function useNavigate() {
6
- const { navigation, setState } = useContext(NavigationContext);
7
+ const { setState } = useContext(NavigationContext);
7
8
  return useMemo(() => {
8
- if (!setState) return navigation;
9
+ if (!setState || !navigation) return navigation;
9
10
  const navigate: Navigation["navigate"] = (destination, options) => {
10
11
  const result = {
11
12
  committed: future(true),
@@ -20,10 +21,7 @@ export function useNavigate() {
20
21
  Promise.resolve().then(() => {
21
22
  try {
22
23
  handle(
23
- navigation.navigate(
24
- new URL(destination, state.url).href,
25
- options,
26
- ),
24
+ navigation.navigate(URI.resolve(state.url, destination), options),
27
25
  );
28
26
  } catch {
29
27
  handle(navigation.navigate(destination, options));
@@ -40,5 +38,5 @@ export function useNavigate() {
40
38
  return typeof value === "function" ? value.bind(target) : value;
41
39
  },
42
40
  });
43
- }, [navigation, setState]);
41
+ }, [setState]);
44
42
  }