}
+
+ );
+
+ expect(result.findByType("h1").props.children).toBe("alex");
+});
+
+it("rejects when a path does not match the regex", () => {
+ const result = testRouteRender(
+ "/users/1234",
+ [a-z]+)/}>
+ {(params) =>
{params.name}
}
+
+ );
+
+ expect(() => result.findByType("h1")).toThrow();
+});
diff --git a/packages/wouter/test/use-route.test.tsx b/packages/wouter/test/use-route.test.tsx
index 0e300deb..9bf0ea2b 100644
--- a/packages/wouter/test/use-route.test.tsx
+++ b/packages/wouter/test/use-route.test.tsx
@@ -1,5 +1,5 @@
import { renderHook, act } from "@testing-library/react";
-import { useRoute, Match, Router } from "wouter";
+import { useRoute, Match, Router, RegexRouteParams } from "wouter";
import { it, expect } from "vitest";
import { memoryLocation } from "wouter/memory-location";
@@ -108,6 +108,14 @@ it("ignores escaped slashes", () => {
});
});
+it("supports regex patterns", () => {
+ assertRoute(/[/]foo/, "/foo", {});
+ assertRoute(/[/]([a-z]+)/, "/bar", { 0: "bar" });
+ assertRoute(/[/]([a-z]+)/, "/123", false);
+ assertRoute(/[/](?[a-z]+)/, "/bar", { 0: "bar", param: "bar" });
+ assertRoute(/[/](?[a-z]+)/, "/123", false);
+});
+
it("reacts to pattern updates", () => {
const { result, rerender } = renderHook(
({ pattern }: { pattern: string }) => useRoute(pattern),
@@ -170,7 +178,7 @@ it("reacts to location updates", () => {
*/
const assertRoute = (
- pattern: string,
+ pattern: string | RegExp,
location: string,
rhs: false | Match | Record
) => {
diff --git a/packages/wouter/types/index.d.ts b/packages/wouter/types/index.d.ts
index 4e9fc8e6..d66ed920 100644
--- a/packages/wouter/types/index.d.ts
+++ b/packages/wouter/types/index.d.ts
@@ -33,11 +33,13 @@ export * from "./router.js";
import { RouteParams } from "regexparam";
+export type RegexRouteParams = { [key: string | number]: string | undefined };
+
/**
* Route patterns and parameters
*/
export interface DefaultParams {
- readonly [paramName: string]: string | undefined;
+ readonly [paramName: string | number]: string | undefined;
}
export type Params = T;
@@ -61,23 +63,33 @@ export interface RouteComponentProps {
export interface RouteProps<
T extends DefaultParams | undefined = undefined,
- RoutePath extends Path = Path
+ RoutePath extends Path | RegExp = Path | RegExp
> {
children?:
| ((
- params: T extends DefaultParams ? T : RouteParams
+ params: T extends DefaultParams
+ ? T
+ : RoutePath extends string
+ ? RouteParams
+ : RegexRouteParams
) => ReactNode)
| ReactNode;
path?: RoutePath;
component?: ComponentType<
- RouteComponentProps>
+ RouteComponentProps<
+ T extends DefaultParams
+ ? T
+ : RoutePath extends string
+ ? RouteParams
+ : RegexRouteParams
+ >
>;
nest?: boolean;
}
export function Route<
T extends DefaultParams | undefined = undefined,
- RoutePath extends Path = Path
+ RoutePath extends Path | RegExp = Path | RegExp
>(props: RouteProps): ReturnType;
/*
@@ -150,10 +162,16 @@ export function useRouter(): RouterObject;
export function useRoute<
T extends DefaultParams | undefined = undefined,
- RoutePath extends Path = Path
+ RoutePath extends Path | RegExp = Path | RegExp
>(
pattern: RoutePath
-): Match>;
+): Match<
+ T extends DefaultParams
+ ? T
+ : RoutePath extends string
+ ? RouteParams
+ : RegexRouteParams
+>;
export function useLocation<
H extends BaseLocationHook = BrowserLocationHook
From b6b5071a8296a9f51be89d7608917afb7f0b84da Mon Sep 17 00:00:00 2001
From: Jonah Henriksson <33059163+JonahPlusPlus@users.noreply.github.com>
Date: Tue, 28 May 2024 14:16:07 -0400
Subject: [PATCH 2/7] Update router.d.ts
---
packages/wouter/types/router.d.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/wouter/types/router.d.ts b/packages/wouter/types/router.d.ts
index d0c1ee21..66c6828e 100644
--- a/packages/wouter/types/router.d.ts
+++ b/packages/wouter/types/router.d.ts
@@ -5,7 +5,7 @@ import {
BaseSearchHook,
} from "./location-hook.js";
-export type Parser = (route: Path) => { pattern: RegExp; keys: string[] };
+export type Parser = (route: Path | RegExp, loose?: boolean) => { pattern: RegExp; keys: string[] };
export type HrefsFormatter = (href: string, router: RouterObject) => string;
From 3e38ab680ce79b85a07714e88443697e92d54e2b Mon Sep 17 00:00:00 2001
From: Jonah Henriksson <33059163+JonahPlusPlus@users.noreply.github.com>
Date: Tue, 28 May 2024 14:37:32 -0400
Subject: [PATCH 3/7] Fixed type issues
---
packages/wouter-preact/types/index.d.ts | 32 ++++++++++++++++++------
packages/wouter-preact/types/router.d.ts | 5 +++-
packages/wouter/test/parser.test.tsx | 2 +-
packages/wouter/types/router.d.ts | 5 +++-
4 files changed, 34 insertions(+), 10 deletions(-)
diff --git a/packages/wouter-preact/types/index.d.ts b/packages/wouter-preact/types/index.d.ts
index 99f5328b..543e5f87 100644
--- a/packages/wouter-preact/types/index.d.ts
+++ b/packages/wouter-preact/types/index.d.ts
@@ -29,11 +29,13 @@ export * from "./router.js";
import { RouteParams } from "regexparam";
+export type RegexRouteParams = { [key: string | number]: string | undefined };
+
/**
* Route patterns and parameters
*/
export interface DefaultParams {
- readonly [paramName: string]: string | undefined;
+ readonly [paramName: string | number]: string | undefined;
}
export type Params = T;
@@ -57,23 +59,33 @@ export interface RouteComponentProps {
export interface RouteProps<
T extends DefaultParams | undefined = undefined,
- RoutePath extends Path = Path
+ RoutePath extends Path | RegExp = Path | RegExp
> {
children?:
| ((
- params: T extends DefaultParams ? T : RouteParams
+ params: T extends DefaultParams
+ ? T
+ : RoutePath extends string
+ ? RouteParams
+ : RegexRouteParams
) => ComponentChildren)
| ComponentChildren;
path?: RoutePath;
component?: ComponentType<
- RouteComponentProps>
+ RouteComponentProps<
+ T extends DefaultParams
+ ? T
+ : RoutePath extends string
+ ? RouteParams
+ : RegexRouteParams
+ >
>;
nest?: boolean;
}
export function Route<
T extends DefaultParams | undefined = undefined,
- RoutePath extends Path = Path
+ RoutePath extends Path | RegExp = Path | RegExp
>(props: RouteProps): ReturnType;
/*
@@ -143,10 +155,16 @@ export function useRouter(): RouterObject;
export function useRoute<
T extends DefaultParams | undefined = undefined,
- RoutePath extends Path = Path
+ RoutePath extends Path | RegExp = Path | RegExp
>(
pattern: RoutePath
-): Match>;
+): Match<
+ T extends DefaultParams
+ ? T
+ : RoutePath extends string
+ ? RouteParams
+ : RegexRouteParams
+>;
export function useLocation<
H extends BaseLocationHook = BrowserLocationHook
diff --git a/packages/wouter-preact/types/router.d.ts b/packages/wouter-preact/types/router.d.ts
index d0c1ee21..66b6da0c 100644
--- a/packages/wouter-preact/types/router.d.ts
+++ b/packages/wouter-preact/types/router.d.ts
@@ -5,7 +5,10 @@ import {
BaseSearchHook,
} from "./location-hook.js";
-export type Parser = (route: Path) => { pattern: RegExp; keys: string[] };
+export type Parser = (
+ route: Path | RegExp,
+ loose?: boolean
+) => { pattern: RegExp; keys: string[] };
export type HrefsFormatter = (href: string, router: RouterObject) => string;
diff --git a/packages/wouter/test/parser.test.tsx b/packages/wouter/test/parser.test.tsx
index a2cbf628..54cd0f06 100644
--- a/packages/wouter/test/parser.test.tsx
+++ b/packages/wouter/test/parser.test.tsx
@@ -7,7 +7,7 @@ import { Router, useRouter, useRoute, Parser } from "wouter";
import { memoryLocation } from "wouter/memory-location";
// Custom parser that uses `path-to-regexp` instead of `regexparam`
-const pathToRegexpParser: Parser = (route: string) => {
+const pathToRegexpParser: Parser = (route: string | RegExp) => {
const keys: Key[] = [];
const pattern = pathToRegexp(route, keys);
diff --git a/packages/wouter/types/router.d.ts b/packages/wouter/types/router.d.ts
index 66c6828e..66b6da0c 100644
--- a/packages/wouter/types/router.d.ts
+++ b/packages/wouter/types/router.d.ts
@@ -5,7 +5,10 @@ import {
BaseSearchHook,
} from "./location-hook.js";
-export type Parser = (route: Path | RegExp, loose?: boolean) => { pattern: RegExp; keys: string[] };
+export type Parser = (
+ route: Path | RegExp,
+ loose?: boolean
+) => { pattern: RegExp; keys: string[] };
export type HrefsFormatter = (href: string, router: RouterObject) => string;
From 7c3db57a685456a8d48c16e04187b9a7c6a00792 Mon Sep 17 00:00:00 2001
From: Jonah Henriksson <33059163+JonahPlusPlus@users.noreply.github.com>
Date: Tue, 28 May 2024 15:15:58 -0400
Subject: [PATCH 4/7] Update README.md
---
README.md | 25 +++++++++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/README.md b/README.md
index 01e6b48a..abe3ba2f 100644
--- a/README.md
+++ b/README.md
@@ -218,6 +218,12 @@ useRoute("/app*");
// optional wildcards, matches "/orders", "/orders/"
// and "/orders/completed/list"
useRoute("/orders/*?");
+
+// regex for matching complex patterns,
+// matches "/hello:123"
+useRoute(/[/]([a-z]+):([0-9]+)[/]?/);
+// and with named capture groups
+useRoute(/[/](?[a-z]+):(?[0-9]+)[/]?/);
```
The second item in the pair `params` is an object with parameters or null if there was no match. For wildcard segments the parameter name is `"*"`:
@@ -317,6 +323,21 @@ const User = () => {
/>
```
+For regex paths, parameters are accessible as indices or through their group name.
+
+```js
+import { Route, useParams } from "wouter";
+
+const User = () => {
+ const params = useParams();
+
+ params.id; // "1"
+ params[0]; // "1"
+};
+
+[0-9]+)[/]?/} component={User}> />
+```
+
### `useSearch`: query strings
Use this hook to get the current search (query) string value. It will cause your component to re-render only when the string itself and not the full location updates. The search string returned **does not** contain a `?` character.
@@ -421,6 +442,10 @@ If you call `useLocation()` inside the last route, it will return `/orders` and
```
+**Note:** The `nest` prop has no effect on regexes passed in.
+It will only determine if nested routes will match the rest of path or match against the same path.
+To make a strict path regex, use regex techniques like `[/]?$` (this matches an optional end slash and the end of the string).
+
### ``
Link component renders an `` element that, when clicked, performs a navigation.
From f90bb2753f8b6e26d5f90ac478846b080bc22488 Mon Sep 17 00:00:00 2001
From: Jonah Henriksson <33059163+JonahPlusPlus@users.noreply.github.com>
Date: Tue, 28 May 2024 15:28:07 -0400
Subject: [PATCH 5/7] Handle RegExp outside of parser
---
packages/wouter-preact/types/router.d.ts | 2 +-
packages/wouter/src/index.js | 14 ++++++++++----
packages/wouter/types/router.d.ts | 2 +-
3 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/packages/wouter-preact/types/router.d.ts b/packages/wouter-preact/types/router.d.ts
index 66b6da0c..370e2067 100644
--- a/packages/wouter-preact/types/router.d.ts
+++ b/packages/wouter-preact/types/router.d.ts
@@ -6,7 +6,7 @@ import {
} from "./location-hook.js";
export type Parser = (
- route: Path | RegExp,
+ route: Path,
loose?: boolean
) => { pattern: RegExp; keys: string[] };
diff --git a/packages/wouter/src/index.js b/packages/wouter/src/index.js
index 1312d334..4d635057 100644
--- a/packages/wouter/src/index.js
+++ b/packages/wouter/src/index.js
@@ -79,15 +79,21 @@ export const useSearch = () => {
};
const matchRoute = (parser, route, path, loose) => {
- // if the route is a regex, `loose` is ignored and
+ // if the input is a regexp, skip parsing
+ const { pattern, keys } = (() => {
+ if (route instanceof RegExp) {
+ return { keys: false, pattern: route };
+ }
+ return parser(route || "*", loose);
+ })();
+
+ // array destructuring loses keys, so this is done in two steps
+ const result = pattern.exec(path) || [];
// when parser is in "loose" mode, `$base` is equal to the
// first part of the route that matches the pattern
// (e.g. for pattern `/a/:b` and path `/a/1/2/3` the `$base` is `a/1`)
// we use this for route nesting
- const { pattern, keys } = parser(route || "*", loose);
- // array destructuring loses keys, so this is done in two steps
- const result = pattern.exec(path) || [];
const [$base, ...matches] = result;
return $base !== undefined
diff --git a/packages/wouter/types/router.d.ts b/packages/wouter/types/router.d.ts
index 66b6da0c..370e2067 100644
--- a/packages/wouter/types/router.d.ts
+++ b/packages/wouter/types/router.d.ts
@@ -6,7 +6,7 @@ import {
} from "./location-hook.js";
export type Parser = (
- route: Path | RegExp,
+ route: Path,
loose?: boolean
) => { pattern: RegExp; keys: string[] };
From 9001d5cfa46fa4f79d27955d98f8136b8deff0d8 Mon Sep 17 00:00:00 2001
From: Jonah Henriksson <33059163+JonahPlusPlus@users.noreply.github.com>
Date: Wed, 29 May 2024 12:20:23 -0400
Subject: [PATCH 6/7] Make requested changes
---
README.md | 13 ++++++------
packages/wouter-preact/types/index.d.ts | 7 ++++---
.../wouter-preact/types/location-hook.d.ts | 2 ++
packages/wouter/src/index.js | 10 ++++------
packages/wouter/test/router.test-d.tsx | 20 ++++++++++++++++++-
packages/wouter/types/index.d.ts | 7 ++++---
packages/wouter/types/location-hook.d.ts | 2 ++
7 files changed, 42 insertions(+), 19 deletions(-)
diff --git a/README.md b/README.md
index abe3ba2f..96e1c522 100644
--- a/README.md
+++ b/README.md
@@ -221,9 +221,9 @@ useRoute("/orders/*?");
// regex for matching complex patterns,
// matches "/hello:123"
-useRoute(/[/]([a-z]+):([0-9]+)[/]?/);
+useRoute(/^[/]([a-z]+):([0-9]+)[/]?$/);
// and with named capture groups
-useRoute(/[/](?[a-z]+):(?[0-9]+)[/]?/);
+useRoute(/^[/](?[a-z]+):(?[0-9]+)[/]?$/);
```
The second item in the pair `params` is an object with parameters or null if there was no match. For wildcard segments the parameter name is `"*"`:
@@ -335,7 +335,7 @@ const User = () => {
params[0]; // "1"
};
-[0-9]+)[/]?/} component={User}> />
+[0-9]+)[/]?$/} component={User}> />
```
### `useSearch`: query strings
@@ -442,9 +442,10 @@ If you call `useLocation()` inside the last route, it will return `/orders` and
```
-**Note:** The `nest` prop has no effect on regexes passed in.
-It will only determine if nested routes will match the rest of path or match against the same path.
-To make a strict path regex, use regex techniques like `[/]?$` (this matches an optional end slash and the end of the string).
+**Note:** The `nest` prop does not alter the regex passed into regex paths.
+Instead, the `nest` prop will only determine if nested routes will match against the rest of path or the same path.
+To make a strict path regex, use a regex pattern like `/^[/](your pattern)[/]?$/` (this matches an optional end slash and the end of the string).
+To make a nestable regex, use a regex pattern like `/^[/](your pattern)(?=$|[/])/` (this matches either the end of the string or a slash for future segments).
### ``
diff --git a/packages/wouter-preact/types/index.d.ts b/packages/wouter-preact/types/index.d.ts
index 543e5f87..f8de84f3 100644
--- a/packages/wouter-preact/types/index.d.ts
+++ b/packages/wouter-preact/types/index.d.ts
@@ -10,6 +10,7 @@ import {
import {
Path,
+ PathPattern,
BaseLocationHook,
HookReturnValue,
HookNavigationOptions,
@@ -59,7 +60,7 @@ export interface RouteComponentProps {
export interface RouteProps<
T extends DefaultParams | undefined = undefined,
- RoutePath extends Path | RegExp = Path | RegExp
+ RoutePath extends PathPattern = PathPattern
> {
children?:
| ((
@@ -85,7 +86,7 @@ export interface RouteProps<
export function Route<
T extends DefaultParams | undefined = undefined,
- RoutePath extends Path | RegExp = Path | RegExp
+ RoutePath extends PathPattern = PathPattern
>(props: RouteProps): ReturnType;
/*
@@ -155,7 +156,7 @@ export function useRouter(): RouterObject;
export function useRoute<
T extends DefaultParams | undefined = undefined,
- RoutePath extends Path | RegExp = Path | RegExp
+ RoutePath extends PathPattern = PathPattern
>(
pattern: RoutePath
): Match<
diff --git a/packages/wouter-preact/types/location-hook.d.ts b/packages/wouter-preact/types/location-hook.d.ts
index ab3ca90c..202ccb2e 100644
--- a/packages/wouter-preact/types/location-hook.d.ts
+++ b/packages/wouter-preact/types/location-hook.d.ts
@@ -4,6 +4,8 @@
export type Path = string;
+export type PathPattern = string | RegExp;
+
export type SearchString = string;
// the base useLocation hook type. Any custom hook (including the
diff --git a/packages/wouter/src/index.js b/packages/wouter/src/index.js
index 4d635057..99262c35 100644
--- a/packages/wouter/src/index.js
+++ b/packages/wouter/src/index.js
@@ -80,12 +80,10 @@ export const useSearch = () => {
const matchRoute = (parser, route, path, loose) => {
// if the input is a regexp, skip parsing
- const { pattern, keys } = (() => {
- if (route instanceof RegExp) {
- return { keys: false, pattern: route };
- }
- return parser(route || "*", loose);
- })();
+ const { pattern, keys } =
+ route instanceof RegExp
+ ? { keys: false, pattern: route }
+ : parser(route || "*", loose);
// array destructuring loses keys, so this is done in two steps
const result = pattern.exec(path) || [];
diff --git a/packages/wouter/test/router.test-d.tsx b/packages/wouter/test/router.test-d.tsx
index 66045542..790f7357 100644
--- a/packages/wouter/test/router.test-d.tsx
+++ b/packages/wouter/test/router.test-d.tsx
@@ -1,6 +1,13 @@
import { ComponentProps } from "react";
import { it, expectTypeOf } from "vitest";
-import { Router, Route, BaseLocationHook, useRouter } from "wouter";
+import {
+ Router,
+ Route,
+ BaseLocationHook,
+ useRouter,
+ Parser,
+ Path,
+} from "wouter";
it("should have at least one child", () => {
// @ts-expect-error
@@ -64,6 +71,17 @@ it("accepts `hrefs` function for transforming href strings", () => {
;
});
+it("accepts `parser` function for generating regular expressions", () => {
+ const parser: Parser = (path: Path, loose?: boolean) => {
+ return {
+ pattern: new RegExp(`^${path}${loose === true ? "(?=$|[/])" : "[/]$"}`),
+ keys: [],
+ };
+ };
+
+ this is a valid router;
+});
+
it("does not accept other props", () => {
const router = useRouter();
diff --git a/packages/wouter/types/index.d.ts b/packages/wouter/types/index.d.ts
index d66ed920..c9c0faf6 100644
--- a/packages/wouter/types/index.d.ts
+++ b/packages/wouter/types/index.d.ts
@@ -14,6 +14,7 @@ import {
import {
Path,
+ PathPattern,
BaseLocationHook,
HookReturnValue,
HookNavigationOptions,
@@ -63,7 +64,7 @@ export interface RouteComponentProps {
export interface RouteProps<
T extends DefaultParams | undefined = undefined,
- RoutePath extends Path | RegExp = Path | RegExp
+ RoutePath extends PathPattern = PathPattern
> {
children?:
| ((
@@ -89,7 +90,7 @@ export interface RouteProps<
export function Route<
T extends DefaultParams | undefined = undefined,
- RoutePath extends Path | RegExp = Path | RegExp
+ RoutePath extends PathPattern = PathPattern
>(props: RouteProps): ReturnType;
/*
@@ -162,7 +163,7 @@ export function useRouter(): RouterObject;
export function useRoute<
T extends DefaultParams | undefined = undefined,
- RoutePath extends Path | RegExp = Path | RegExp
+ RoutePath extends PathPattern = PathPattern
>(
pattern: RoutePath
): Match<
diff --git a/packages/wouter/types/location-hook.d.ts b/packages/wouter/types/location-hook.d.ts
index 9bcfc3c9..0d1aac14 100644
--- a/packages/wouter/types/location-hook.d.ts
+++ b/packages/wouter/types/location-hook.d.ts
@@ -4,6 +4,8 @@
export type Path = string;
+export type PathPattern = string | RegExp;
+
export type SearchString = string;
// the base useLocation hook type. Any custom hook (including the
From 44181fbdf43b95c9aa2e0ff29921b65285e064af Mon Sep 17 00:00:00 2001
From: Jonah Henriksson <33059163+JonahPlusPlus@users.noreply.github.com>
Date: Wed, 29 May 2024 13:31:05 -0400
Subject: [PATCH 7/7] Make params consistent for string paths
---
README.md | 5 +-
packages/wouter-preact/types/index.d.ts | 11 ++-
packages/wouter/src/index.js | 18 ++--
packages/wouter/test/parser.test.tsx | 4 +-
packages/wouter/test/use-params.test-d.ts | 8 +-
packages/wouter/test/use-params.test.tsx | 33 ++++++-
packages/wouter/test/use-route.test-d.ts | 3 +
packages/wouter/test/use-route.test.tsx | 107 +++++++++++++++++-----
packages/wouter/types/index.d.ts | 11 ++-
9 files changed, 150 insertions(+), 50 deletions(-)
diff --git a/README.md b/README.md
index 96e1c522..0be0e9c3 100644
--- a/README.md
+++ b/README.md
@@ -318,12 +318,15 @@ const User = () => {
const params = useParams();
params.id; // "1"
+
+ // alternatively, use the index to access the prop
+ params[0]; // "1"
};
/>
```
-For regex paths, parameters are accessible as indices or through their group name.
+It is the same for regex paths. Capture groups can be accessed by their index, or if there is a named capture group, that can be used instead.
```js
import { Route, useParams } from "wouter";
diff --git a/packages/wouter-preact/types/index.d.ts b/packages/wouter-preact/types/index.d.ts
index f8de84f3..aa5030b4 100644
--- a/packages/wouter-preact/types/index.d.ts
+++ b/packages/wouter-preact/types/index.d.ts
@@ -30,6 +30,9 @@ export * from "./router.js";
import { RouteParams } from "regexparam";
+export type StringRouteParams = RouteParams & {
+ [param: number]: string | undefined;
+};
export type RegexRouteParams = { [key: string | number]: string | undefined };
/**
@@ -67,7 +70,7 @@ export interface RouteProps<
params: T extends DefaultParams
? T
: RoutePath extends string
- ? RouteParams
+ ? StringRouteParams
: RegexRouteParams
) => ComponentChildren)
| ComponentChildren;
@@ -77,7 +80,7 @@ export interface RouteProps<
T extends DefaultParams
? T
: RoutePath extends string
- ? RouteParams
+ ? StringRouteParams
: RegexRouteParams
>
>;
@@ -163,7 +166,7 @@ export function useRoute<
T extends DefaultParams
? T
: RoutePath extends string
- ? RouteParams
+ ? StringRouteParams
: RegexRouteParams
>;
@@ -176,7 +179,7 @@ export function useSearch<
>(): ReturnType;
export function useParams(): T extends string
- ? RouteParams
+ ? StringRouteParams
: T extends undefined
? DefaultParams
: T;
diff --git a/packages/wouter/src/index.js b/packages/wouter/src/index.js
index 99262c35..b5b28ce5 100644
--- a/packages/wouter/src/index.js
+++ b/packages/wouter/src/index.js
@@ -99,20 +99,22 @@ const matchRoute = (parser, route, path, loose) => {
true,
(() => {
- /// for regex paths, `keys` will always be false
- if (keys !== false) {
- // an object with parameters matched, e.g. { foo: "bar" } for "/:foo"
- // we "zip" two arrays here to construct the object
- // ["foo"], ["bar"] → { foo: "bar" }
- return Object.fromEntries(keys.map((key, i) => [key, matches[i]]));
- }
+ // for regex paths, `keys` will always be false
+
+ // an object with parameters matched, e.g. { foo: "bar" } for "/:foo"
+ // we "zip" two arrays here to construct the object
+ // ["foo"], ["bar"] → { foo: "bar" }
+ const groups =
+ keys !== false
+ ? Object.fromEntries(keys.map((key, i) => [key, matches[i]]))
+ : result.groups;
// convert the array to an instance of object
// this makes it easier to integrate with the existing param implementation
let obj = { ...matches };
// merge named capture groups with matches array
- result.groups && Object.assign(obj, result.groups);
+ groups && Object.assign(obj, groups);
return obj;
})(),
diff --git a/packages/wouter/test/parser.test.tsx b/packages/wouter/test/parser.test.tsx
index 54cd0f06..8b595dd9 100644
--- a/packages/wouter/test/parser.test.tsx
+++ b/packages/wouter/test/parser.test.tsx
@@ -7,7 +7,7 @@ import { Router, useRouter, useRoute, Parser } from "wouter";
import { memoryLocation } from "wouter/memory-location";
// Custom parser that uses `path-to-regexp` instead of `regexparam`
-const pathToRegexpParser: Parser = (route: string | RegExp) => {
+const pathToRegexpParser: Parser = (route: string) => {
const keys: Key[] = [];
const pattern = pathToRegexp(route, keys);
@@ -42,6 +42,6 @@ it("allows to change the behaviour of route matching", () => {
expect(result.current).toStrictEqual([
true,
- { pages: undefined, rest: "10/bio", 0: "home" },
+ { 0: "home", 1: undefined, 2: "10/bio", pages: undefined, rest: "10/bio" },
]);
});
diff --git a/packages/wouter/test/use-params.test-d.ts b/packages/wouter/test/use-params.test-d.ts
index 29d4de1e..972c379d 100644
--- a/packages/wouter/test/use-params.test-d.ts
+++ b/packages/wouter/test/use-params.test-d.ts
@@ -10,12 +10,18 @@ it("returns an object with arbitrary parameters", () => {
expectTypeOf(params).toBeObject();
expectTypeOf(params.any).toEqualTypeOf();
+ expectTypeOf(params[0]).toEqualTypeOf();
});
it("can infer the type of parameters from the route path", () => {
const params = useParams<"/app/users/:name?/:id">();
- expectTypeOf(params).toMatchTypeOf<{ id: string; name?: string }>();
+ expectTypeOf(params).toMatchTypeOf<{
+ 0?: string;
+ 1?: string;
+ id: string;
+ name?: string;
+ }>();
});
it("can accept the custom type of parameters as a generic argument", () => {
diff --git a/packages/wouter/test/use-params.test.tsx b/packages/wouter/test/use-params.test.tsx
index 7528ce1d..409d6d75 100644
--- a/packages/wouter/test/use-params.test.tsx
+++ b/packages/wouter/test/use-params.test.tsx
@@ -18,7 +18,10 @@ it("contains a * parameter when used inside an empty ", () => {
),
});
- expect(result.current).toEqual({ "*": "app-2/goods/tees" });
+ expect(result.current).toEqual({
+ 0: "app-2/goods/tees",
+ "*": "app-2/goods/tees",
+ });
});
it("returns an empty object when there are no params", () => {
@@ -40,7 +43,12 @@ it("returns parameters from the closest parent match", () => {
),
});
- expect(result.current).toEqual({ id: "1", name: "maria" });
+ expect(result.current).toEqual({
+ 0: "1",
+ 1: "maria",
+ id: "1",
+ name: "maria",
+ });
});
it("rerenders with parameters change", () => {
@@ -57,10 +65,20 @@ it("rerenders with parameters change", () => {
expect(result.current).toBeNull();
act(() => navigate("/posts/all"));
- expect(result.current).toEqual({ a: "posts", b: "all" });
+ expect(result.current).toEqual({
+ 0: "posts",
+ 1: "all",
+ a: "posts",
+ b: "all",
+ });
act(() => navigate("/posts/latest"));
- expect(result.current).toEqual({ a: "posts", b: "latest" });
+ expect(result.current).toEqual({
+ 0: "posts",
+ 1: "latest",
+ a: "posts",
+ b: "latest",
+ });
});
it("extracts parameters of the nested route", () => {
@@ -79,5 +97,10 @@ it("extracts parameters of the nested route", () => {
),
});
- expect(result.current).toEqual({ version: "v2", chain: "eth" });
+ expect(result.current).toEqual({
+ 0: "v2",
+ 1: "eth",
+ version: "v2",
+ chain: "eth",
+ });
});
diff --git a/packages/wouter/test/use-route.test-d.ts b/packages/wouter/test/use-route.test-d.ts
index 9836f7e0..8638ea86 100644
--- a/packages/wouter/test/use-route.test-d.ts
+++ b/packages/wouter/test/use-route.test-d.ts
@@ -40,6 +40,9 @@ it("infers parameters from the route path", () => {
if (inferedParams) {
expectTypeOf(inferedParams).toMatchTypeOf<{
+ 0?: string;
+ 1?: string;
+ 2?: string;
name?: string;
id: string;
wildcard?: string;
diff --git a/packages/wouter/test/use-route.test.tsx b/packages/wouter/test/use-route.test.tsx
index 9bf0ea2b..61e3a850 100644
--- a/packages/wouter/test/use-route.test.tsx
+++ b/packages/wouter/test/use-route.test.tsx
@@ -6,21 +6,24 @@ import { memoryLocation } from "wouter/memory-location";
it("is case insensitive", () => {
assertRoute("/Users", "/users", {});
assertRoute("/HomePage", "/Homepage", {});
- assertRoute("/Users/:Name", "/users/alex", { Name: "alex" });
+ assertRoute("/Users/:Name", "/users/alex", { 0: "alex", Name: "alex" });
});
it("supports required segments", () => {
- assertRoute("/:page", "/users", { page: "users" });
+ assertRoute("/:page", "/users", { 0: "users", page: "users" });
assertRoute("/:page", "/users/all", false);
- assertRoute("/:page", "/1", { page: "1" });
+ assertRoute("/:page", "/1", { 0: "1", page: "1" });
- assertRoute("/home/:page/etc", "/home/users/etc", { page: "users" });
+ assertRoute("/home/:page/etc", "/home/users/etc", {
+ 0: "users",
+ page: "users",
+ });
assertRoute("/home/:page/etc", "/home/etc", false);
assertRoute(
"/root/payments/:id/refunds/:refId",
"/root/payments/1/refunds/2",
- [true, { id: "1", refId: "2" }]
+ [true, { 0: "1", 1: "2", id: "1", refId: "2" }]
);
});
@@ -31,22 +34,41 @@ it("ignores the trailing slash", () => {
assertRoute("/home/", "/home/", {});
assertRoute("/home/", "/home", {});
- assertRoute("/:page", "/users/", [true, { page: "users" }]);
- assertRoute("/catalog/:section?", "/catalog/", { section: undefined });
+ assertRoute("/:page", "/users/", [true, { 0: "users", page: "users" }]);
+ assertRoute("/catalog/:section?", "/catalog/", {
+ 0: undefined,
+ section: undefined,
+ });
});
it("supports trailing wildcards", () => {
- assertRoute("/app/*", "/app/", { "*": "" });
- assertRoute("/app/*", "/app/dashboard/intro", { "*": "dashboard/intro" });
- assertRoute("/app/*", "/app/charges/1", { "*": "charges/1" });
+ assertRoute("/app/*", "/app/", { 0: "", "*": "" });
+ assertRoute("/app/*", "/app/dashboard/intro", {
+ 0: "dashboard/intro",
+ "*": "dashboard/intro",
+ });
+ assertRoute("/app/*", "/app/charges/1", { 0: "charges/1", "*": "charges/1" });
});
it("supports wildcards in the middle of the pattern", () => {
- assertRoute("/app/*/settings", "/app/users/settings", { "*": "users" });
- assertRoute("/app/*/settings", "/app/users/1/settings", { "*": "users/1" });
+ assertRoute("/app/*/settings", "/app/users/settings", {
+ 0: "users",
+ "*": "users",
+ });
+ assertRoute("/app/*/settings", "/app/users/1/settings", {
+ 0: "users/1",
+ "*": "users/1",
+ });
- assertRoute("/*/payments/:id", "/home/payments/1", { "*": "home", id: "1" });
+ assertRoute("/*/payments/:id", "/home/payments/1", {
+ 0: "home",
+ 1: "1",
+ "*": "home",
+ id: "1",
+ });
assertRoute("/*/payments/:id?", "/home/payments", {
+ 0: "home",
+ 1: undefined,
"*": "home",
id: undefined,
});
@@ -54,56 +76,78 @@ it("supports wildcards in the middle of the pattern", () => {
it("uses a question mark to define optional segments", () => {
assertRoute("/books/:genre/:title?", "/books/scifi", {
+ 0: "scifi",
+ 1: undefined,
genre: "scifi",
title: undefined,
});
assertRoute("/books/:genre/:title?", "/books/scifi/dune", {
+ 0: "scifi",
+ 1: "dune",
genre: "scifi",
title: "dune",
});
assertRoute("/books/:genre/:title?", "/books/scifi/dune/all", false);
assertRoute("/app/:company?/blog/:post", "/app/apple/blog/mac", {
+ 0: "apple",
+ 1: "mac",
company: "apple",
post: "mac",
});
assertRoute("/app/:company?/blog/:post", "/app/blog/mac", {
+ 0: undefined,
+ 1: "mac",
company: undefined,
post: "mac",
});
});
it("supports optional wildcards", () => {
- assertRoute("/app/*?", "/app/blog/mac", { "*": "blog/mac" });
- assertRoute("/app/*?", "/app", { "*": undefined });
- assertRoute("/app/*?/dashboard", "/app/v1/dashboard", { "*": "v1" });
- assertRoute("/app/*?/dashboard", "/app/dashboard", { "*": undefined });
+ assertRoute("/app/*?", "/app/blog/mac", { 0: "blog/mac", "*": "blog/mac" });
+ assertRoute("/app/*?", "/app", { 0: undefined, "*": undefined });
+ assertRoute("/app/*?/dashboard", "/app/v1/dashboard", { 0: "v1", "*": "v1" });
+ assertRoute("/app/*?/dashboard", "/app/dashboard", {
+ 0: undefined,
+ "*": undefined,
+ });
assertRoute("/app/*?/users/:name", "/app/users/karen", {
+ 0: undefined,
+ 1: "karen",
"*": undefined,
name: "karen",
});
});
it("supports other characters in segments", () => {
- assertRoute("/users/:name", "/users/1-alex", { name: "1-alex" });
+ assertRoute("/users/:name", "/users/1-alex", { 0: "1-alex", name: "1-alex" });
assertRoute("/staff/:name/:bio?", "/staff/John Doe 3", {
+ 0: "John Doe 3",
+ 1: undefined,
name: "John Doe 3",
bio: undefined,
});
assertRoute("/staff/:name/:bio?", "/staff/John Doe 3/bio", {
+ 0: "John Doe 3",
+ 1: "bio",
name: "John Doe 3",
bio: "bio",
});
assertRoute("/users/:name/bio", "/users/$102_Kathrine&/bio", {
+ 0: "$102_Kathrine&",
name: "$102_Kathrine&",
});
});
it("ignores escaped slashes", () => {
- assertRoute("/:param/bar", "/foo%2Fbar/bar", { param: "foo%2Fbar" });
+ assertRoute("/:param/bar", "/foo%2Fbar/bar", {
+ 0: "foo%2Fbar",
+ param: "foo%2Fbar",
+ });
assertRoute("/:param", "/foo%2Fbar%D1%81%D0%B0%D0%BD%D1%8F", {
+ 0: "foo%2Fbarсаня",
param: "foo%2Fbarсаня",
});
});
@@ -138,17 +182,27 @@ it("reacts to pattern updates", () => {
rerender({ pattern: "/blog/:category/:post/:action" });
expect(result.current).toStrictEqual([
true,
- { category: "products", post: "40", action: "read-all" },
+ {
+ 0: "products",
+ 1: "40",
+ 2: "read-all",
+ category: "products",
+ post: "40",
+ action: "read-all",
+ },
]);
rerender({ pattern: "/blog/products/:id?/read-all" });
- expect(result.current).toStrictEqual([true, { id: "40" }]);
+ expect(result.current).toStrictEqual([true, { 0: "40", id: "40" }]);
rerender({ pattern: "/blog/products/:name" });
expect(result.current).toStrictEqual([false, null]);
rerender({ pattern: "/blog/*" });
- expect(result.current).toStrictEqual([true, { "*": "products/40/read-all" }]);
+ expect(result.current).toStrictEqual([
+ true,
+ { 0: "products/40/read-all", "*": "products/40/read-all" },
+ ]);
});
it("reacts to location updates", () => {
@@ -161,16 +215,19 @@ it("reacts to location updates", () => {
expect(result.current).toStrictEqual([false, null]);
act(() => navigate("/cities/berlin"));
- expect(result.current).toStrictEqual([true, { city: "berlin" }]);
+ expect(result.current).toStrictEqual([true, { 0: "berlin", city: "berlin" }]);
act(() => navigate("/cities/Tokyo"));
- expect(result.current).toStrictEqual([true, { city: "Tokyo" }]);
+ expect(result.current).toStrictEqual([true, { 0: "Tokyo", city: "Tokyo" }]);
act(() => navigate("/about"));
expect(result.current).toStrictEqual([false, null]);
act(() => navigate("/cities"));
- expect(result.current).toStrictEqual([true, { city: undefined }]);
+ expect(result.current).toStrictEqual([
+ true,
+ { 0: undefined, city: undefined },
+ ]);
});
/**
diff --git a/packages/wouter/types/index.d.ts b/packages/wouter/types/index.d.ts
index c9c0faf6..449c1337 100644
--- a/packages/wouter/types/index.d.ts
+++ b/packages/wouter/types/index.d.ts
@@ -34,6 +34,9 @@ export * from "./router.js";
import { RouteParams } from "regexparam";
+export type StringRouteParams = RouteParams & {
+ [param: number]: string | undefined;
+};
export type RegexRouteParams = { [key: string | number]: string | undefined };
/**
@@ -71,7 +74,7 @@ export interface RouteProps<
params: T extends DefaultParams
? T
: RoutePath extends string
- ? RouteParams
+ ? StringRouteParams
: RegexRouteParams
) => ReactNode)
| ReactNode;
@@ -81,7 +84,7 @@ export interface RouteProps<
T extends DefaultParams
? T
: RoutePath extends string
- ? RouteParams
+ ? StringRouteParams
: RegexRouteParams
>
>;
@@ -170,7 +173,7 @@ export function useRoute<
T extends DefaultParams
? T
: RoutePath extends string
- ? RouteParams
+ ? StringRouteParams
: RegexRouteParams
>;
@@ -183,7 +186,7 @@ export function useSearch<
>(): ReturnType;
export function useParams(): T extends string
- ? RouteParams
+ ? StringRouteParams
: T extends undefined
? DefaultParams
: T;