@aminnairi/react-router 0.1.0 → 1.0.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.
Files changed (3) hide show
  1. package/README.md +481 -90
  2. package/index.tsx +149 -62
  3. package/package.json +5 -3
package/README.md CHANGED
@@ -40,7 +40,11 @@ import { createPage } from "@aminnairi/react-router";
40
40
 
41
41
  export const home = createPage({
42
42
  path: "/",
43
- element: () => <h1>Home page</h1>
43
+ element: function Home() {
44
+ return (
45
+ <h1>Home page</h1>
46
+ );
47
+ }
44
48
  });
45
49
  ```
46
50
 
@@ -49,11 +53,14 @@ touch src/router/fallback.tsx
49
53
  ```
50
54
 
51
55
  ```tsx
56
+ import { useNavigateToPage } from "@aminnairi/react-router";
52
57
  import { home } from "./pages/home";
53
58
 
54
59
  export const Fallback = () => {
60
+ const navigateToHomePage = useNavigateToPage(home);
61
+
55
62
  return (
56
- <button onClick={home.navigate}>
63
+ <button onClick={navigateToHomePage}>
57
64
  Go back home
58
65
  </button>
59
66
  );
@@ -66,9 +73,12 @@ touch src/router/issue.tsx
66
73
 
67
74
  ```tsx
68
75
  import { Fragment } from "react";
76
+ import { useNavigateToPage } from "@aminnairi/react-router";
69
77
  import { home } from "./pages/home";
70
78
 
71
79
  export const Issue = () => {
80
+ const navigateToHomePage = useNavigateToPage(home);
81
+
72
82
  return (
73
83
  <Fragment>
74
84
  <h1>An issue occurred</h1>
@@ -93,8 +103,8 @@ import { home } from "./router/pages/home";
93
103
  export const router = createRouter({
94
104
  fallback: Fallback,
95
105
  issue: Issue,
96
- routes: [
97
- home.page
106
+ pages: [
107
+ home
98
108
  ]
99
109
  });
100
110
  ```
@@ -113,6 +123,31 @@ export default function App() {
113
123
  }
114
124
  ```
115
125
 
126
+ ```bash
127
+ touch src/main.tsx
128
+ ```
129
+
130
+ ```tsx
131
+ import { StrictMode } from "react";
132
+ import { createRoot } from "react-dom/client";
133
+ import { router } from "./router";
134
+ import App from "./App";
135
+
136
+ const rootElement = document.getElementById("root");
137
+
138
+ if (!rootElement) {
139
+ throw new Error("Root element not found");
140
+ }
141
+
142
+ createRoot(rootElement).render(
143
+ <StrictMode>
144
+ <router.Provider>
145
+ <App />
146
+ </router.Provider>
147
+ </StrictMode>
148
+ );
149
+ ```
150
+
116
151
  ### Startup
117
152
 
118
153
  ```bash
@@ -130,9 +165,11 @@ import { createPage } from "@aminnairi/react-router";
130
165
 
131
166
  createPage({
132
167
  path: "/",
133
- element: () => (
134
- <h1>Home</h1>
135
- )
168
+ element: function Home() {
169
+ return (
170
+ <h1>Home</h1>
171
+ );
172
+ }
136
173
  });
137
174
  ```
138
175
 
@@ -143,11 +180,11 @@ import { createPage, createRouter } from "@aminnairi/react-router";
143
180
 
144
181
  const home = createPage({
145
182
  path: "/",
146
- element: () => (
147
- <h1>
148
- Home
149
- </h1>
150
- )
183
+ element: function Home() {
184
+ return (
185
+ <h1>Home</h1>
186
+ );
187
+ }
151
188
  });
152
189
 
153
190
  createRouter({
@@ -161,9 +198,7 @@ createRouter({
161
198
  An error occurred
162
199
  </h1>
163
200
  ),
164
- pages: [
165
- home.page
166
- ]
201
+ pages: [home]
167
202
  });
168
203
  ```
169
204
 
@@ -174,69 +209,80 @@ import { createPage } from "@aminnairi/react-router";
174
209
 
175
210
  createPage({
176
211
  path: "/users/:user",
177
- element: ({ parameters: { user }}) => (
178
- <h1>
179
- User#{user}
180
- </h1>
181
- )
212
+ element: function User({ parameters: { user }}) {
213
+ return (
214
+ <h1>User#{user}</h1>
215
+ );
216
+ }
182
217
  });
183
218
  ```
184
219
 
185
- And if you can have of course more than one dynamic parameter.
220
+ And you can have of course more than one dynamic parameter.
186
221
 
187
222
  ```tsx
188
223
  import { createPage } from "@aminnairi/react-router";
189
224
 
190
225
  createPage({
191
226
  path: "/users/:user/articles/:article",
192
- element: ({ parameters: { user, article }}) => (
193
- <h1>
194
- Article#{article } of user#{user}
195
- </h1>
196
- )
227
+ element: function UserArticle({ parameters: { user, article }}) {
228
+ return (
229
+ <h1>Article#{article } of user#{user}</h1>
230
+ );
231
+ }
197
232
  });
198
233
  ```
199
234
 
200
- You can also navigate to one page from another.
235
+ ### useNavigateToPage
236
+
237
+ You can navigate from one page from another.
201
238
 
202
239
  ```tsx
203
240
  import { Fragment } from "react";
204
- import { createPage } from "@aminnairi/react-router";
241
+ import { createPage, useNavigateToPage } from "@aminnairi/react-router";
205
242
 
206
243
  const login = createPage({
207
244
  path: "/login",
208
- element: () => (
209
- <h1>
210
- Login
211
- </h1>
212
- )
245
+ element: function Login() {
246
+ return (
247
+ <h1>Login</h1>
248
+ );
249
+ }
213
250
  });
214
251
 
215
252
  const about = createPage({
216
253
  path: "/about",
217
- element: () => (
218
- <Fragment>
219
- <h1>
220
- About Us
221
- </h1>
222
- <button onClick={() => login.navigate({})}>
223
- </button>
224
- </Fragment>
225
- )
254
+ element: function About() {
255
+ const navigateToLoginPage = useNavigateToPage(login);
256
+
257
+ return (
258
+ <Fragment>
259
+ <h1>
260
+ About Us
261
+ </h1>
262
+ <button onClick={navigateToLoginPage}>
263
+ Login
264
+ </button>
265
+ </Fragment>
266
+ );
267
+ }
226
268
  });
227
269
 
228
270
  createPage({
229
271
  path: "/",
230
- element: () => (
231
- <Fragment>
232
- <h1>
233
- Home
234
- </h1>
235
- <button onClick={about.navigate}>
236
- About Us
237
- </button>
238
- </Fragment>
239
- )
272
+ element: function Home() {
273
+ const navigateToAboutPage = useNavigateToPage(about);
274
+
275
+ return (
276
+ <Fragment>
277
+ <h1>
278
+ Home
279
+ </h1>
280
+ <button onClick={navigateToAboutPage}>
281
+ About Us
282
+ </button>
283
+ </Fragment>
284
+ );
285
+ }
240
286
  });
241
287
  ```
242
288
 
@@ -244,29 +290,33 @@ And you can of course navigate to pages that have dynamic parameters as well.
244
290
 
245
291
  ```tsx
246
292
  import { Fragment } from "react";
247
- import { createPage } from "@aminnairi/react-router";
293
+ import { createPage, useNavigateToPage } from "@aminnairi/react-router";
248
294
 
249
295
  const user = createPage({
250
296
  path: "/users/:user",
251
- element: ({ parameters: { user }}) => (
252
- <h1>
253
- User#{user}
254
- </h1>
255
- )
297
+ element: function User({ parameters: { user }}) {
298
+ return (
299
+ <h1>User#{user}</h1>
300
+ );
301
+ }
256
302
  });
257
303
 
258
304
  createPage({
259
305
  path: "/",
260
- element: () => (
261
- <Fragment>
262
- <h1>
263
- Home
264
- </h1>
265
- <button onClick={() => user.navigate({ user: "123" })}>
266
- User#123
267
- </button>
268
- </Fragment>
269
- )
306
+ element: function Home() {
307
+ const navigateToUserPage = useNavigateToPage(user);
308
+
309
+ return (
310
+ <Fragment>
311
+ <h1>
312
+ Home
313
+ </h1>
314
+ <button onClick={() => navigateToUserPage({ user: "123" })}>
315
+ User#123
316
+ </button>
317
+ </Fragment>
318
+ );
319
+ }
270
320
  });
271
321
  ```
272
322
 
@@ -281,9 +331,11 @@ import { createRouter, createPage } from "@aminnairi/react-router";
281
331
 
282
332
  const home = createPage({
283
333
  path: "/",
284
- element: () => (
285
- <h1>Home</h1>
286
- )
334
+ element: function Home() {
335
+ return (
336
+ <h1>Home</h1>
337
+ );
338
+ }
287
339
  });
288
340
 
289
341
  const router = createRouter({
@@ -294,7 +346,7 @@ const router = createRouter({
294
346
  <h1>An error occurred</h1>
295
347
  ),
296
348
  pages: [
297
- home.page
349
+ home
298
350
  ]
299
351
  });
300
352
 
@@ -324,7 +376,9 @@ const App = () => {
324
376
 
325
377
  root.render(
326
378
  <StrictMode>
327
- <App />
379
+ <router.Provider>
380
+ <App />
381
+ </router.Provider>
328
382
  </StrictMode>
329
383
  );
330
384
  ```
@@ -340,9 +394,11 @@ import { createRouter, createPage } from "@aminnairi/react-router";
340
394
 
341
395
  const home = createPage({
342
396
  path: "/",
343
- element: () => (
344
- <h1>Home</h1>
345
- )
397
+ element: function Page() {
398
+ return (
399
+ <h1>Home</h1>
400
+ );
401
+ }
346
402
  });
347
403
 
348
404
  const router = createRouter({
@@ -354,7 +410,7 @@ const router = createRouter({
354
410
  <h1>An error occurred</h1>
355
411
  ),
356
412
  pages: [
357
- home.page
413
+ home
358
414
  ]
359
415
  });
360
416
 
@@ -384,7 +440,9 @@ const App = () => {
384
440
 
385
441
  root.render(
386
442
  <StrictMode>
387
- <App />
443
+ <router.Provider>
444
+ <App />
445
+ </router.Provider>
388
446
  </StrictMode>
389
447
  );
390
448
  ```
@@ -398,9 +456,11 @@ import { createRouter, createPage } from "@aminnairi/react-router";
398
456
 
399
457
  const home = createPage({
400
458
  path: "/",
401
- element: () => (
402
- <h1>Home</h1>
403
- )
459
+ element: function Home() {
460
+ return (
461
+ <h1>Home</h1>
462
+ );
463
+ }
404
464
  });
405
465
 
406
466
  const router = createRouter({
@@ -418,7 +478,7 @@ const router = createRouter({
418
478
  );
419
479
  ),
420
480
  pages: [
421
- home.page
481
+ home
422
482
  ]
423
483
  });
424
484
 
@@ -448,7 +508,9 @@ const App = () => {
448
508
 
449
509
  root.render(
450
510
  <StrictMode>
451
- <App />
511
+ <router.Provider>
512
+ <App />
513
+ </router.Provider>
452
514
  </StrictMode>
453
515
  );
454
516
  ```
@@ -462,9 +524,11 @@ import { createRouter, createPage, createIssue } from "@aminnairi/react-router";
462
524
 
463
525
  const home = createPage({
464
526
  path: "/",
465
- element: () => (
466
- <h1>Home</h1>
467
- )
527
+ element: function Home() {
528
+ return (
529
+ <h1>Home</h1>
530
+ );
531
+ }
468
532
  });
469
533
 
470
534
  const Fallback = () => {
@@ -488,7 +552,89 @@ const router = createRouter({
488
552
  fallback: Fallback,
489
553
  issue: Issue,
490
554
  pages: [
491
- home.page
555
+ home
556
+ ]
557
+ });
558
+
559
+ const rootElement = document.getElementById("root");
560
+
561
+ if (!rootElement) {
562
+ throw new Error("Root element not found");
563
+ }
564
+
565
+ const root = createRoot(rootElement);
566
+
567
+ const App = () => {
568
+ return (
569
+ <Fragment>
570
+ <header>
571
+ <h1>App</h1>
572
+ </header>
573
+ <main>
574
+ <router.View />
575
+ </main>
576
+ <footer>
577
+ Credit © Yourself 2025
578
+ </footer>
579
+ </Fragment>
580
+ );
581
+ }
582
+
583
+ root.render(
584
+ <StrictMode>
585
+ <App />
586
+ </StrictMode>
587
+ );
588
+ ```
589
+
590
+ You can use a prefix for your routes, useful if you need to publish this app in a scope like GitHub Pages.
591
+
592
+ You don't have to manually append this prefix when creating pages, its automatically added for you.
593
+
594
+ ```tsx
595
+ import { Fragment, StrictMode } from "react";
596
+ import { createRoot } from "react-dom/client";
597
+ import { createRouter, createPage, createIssue, useNavigateToPage } from "@aminnairi/react-router";
598
+
599
+ const home = createPage({
600
+ path: "/",
601
+ element: function Home() {
602
+ return (
603
+ <h1>Home</h1>
604
+ );
605
+ }
606
+ });
607
+
608
+ const Fallback = () => {
609
+ const navigateToHomePage = useNavigateToPage(home);
610
+
611
+ return (
612
+ <Fragment>
613
+ <h1>Not found</h1>
614
+ <button onClick={navigateToHomePage}>
615
+ Go Back Home
616
+ </button>
617
+ </Fragment>
618
+ );
619
+ }
620
+
621
+ const Issue = createIssue(({ error, reset }) => (
622
+ return (
623
+ <Fragment>
624
+ <h1>Error</h1>
625
+ <p>{error.message}</p>
626
+ <button onClick={reset}>Reset</button>
627
+ </Fragment>
628
+ );
629
+ ));
630
+
631
+ const router = createRouter({
632
+ prefix: "/portfolio",
633
+ transition: true,
634
+ fallback: Fallback,
635
+ issue: Issue,
636
+ pages: [
637
+ home
492
638
  ]
493
639
  });
494
640
 
@@ -523,6 +669,119 @@ root.render(
523
669
  );
524
670
  ```
525
671
 
672
+ ### useNavigateToPage
673
+
674
+ Allow you to create a function that can then be called to navigate to another page inside a React component.
675
+
676
+ It accepts a page that has been created using `createPage`.
677
+
678
+ ```tsx
679
+ import { Fragment } from "react";
680
+ import { createPage, useNavigateToPage } from "@aminnairi/react-router";
681
+
682
+ const home = createPath({
683
+ path: "/",
684
+ element: function Home() {
685
+ return (
686
+ <h1>Home</h1>
687
+ );
688
+ }
689
+ });
690
+
691
+ createPage({
692
+ path: "/about",
693
+ element: function About() {
694
+ const navigateToHomePage = useNavigateToPage(home);
695
+
696
+ return (
697
+ <Fragment>
698
+ <h1>About</h1>
699
+ <button onClick={navigateToHomePage}>Home</button>
700
+ </Fragment>
701
+ );
702
+ }
703
+ });
704
+ ```
705
+
706
+ If this page has dynamic parameters, it forces you to provide them when called inside your component.
707
+
708
+ The parameters should always be provided as string, as they are the only data type that can be used inside a URL.
709
+
710
+ ```tsx
711
+ import { Fragment } from "react";
712
+ import { createPage, useNavigateToPage } from "@aminnairi/react-router";
713
+
714
+ const user = createPath({
715
+ path: "/users/:user",
716
+ element: function User({ parameters: { user }}) {
717
+ return (
718
+ <h1>User#{user}</h1>
719
+ );
720
+ }
721
+ });
722
+
723
+ createPage({
724
+ path: "/about",
725
+ element: function About() {
726
+ const navigateToUserPage = useNavigateToPage(user);
727
+
728
+ return (
729
+ <Fragment>
730
+ <h1>About</h1>
731
+ <button onClick={() => navigateToUserPage({ user: "123" })}>Home</button>
732
+ </Fragment>
733
+ );
734
+ }
735
+ });
736
+ ```
737
+
738
+ ### useSearch
739
+
740
+ Allow you to get one or more search query from the URL.
741
+
742
+ This will return an instance of the `URLSearchParams` Web API so that you can use you existing knowledge to manipulate the search queries easily.
743
+
744
+ ```tsx
745
+ import { useMemo } from "react";
746
+ import { createPage, useSearch } from "@aminnairi/react-router";
747
+
748
+ createPage({
749
+ path: "/users",
750
+ element: function Home() {
751
+ const [search] = useSearch();
752
+ const sortedByDate = useMemo(() => search.get("sort-by") === "date", [search]);
753
+
754
+ return (
755
+ <h1>Users</h1>
756
+ <p>Sorted by date: {sortedByDate ? "yes" : "no"}</p>
757
+ );
758
+ }
759
+ });
760
+ ```
761
+
762
+ You cannot set the search queries for now, this will be added in future release of this library.
763
+
764
+ ### useHash
765
+
766
+ Allow you to get the hash, also called fragment, from the URL which is everything after the `#` symbol.
767
+
768
+ ```tsx
769
+ import { createPage, useHash } from "@aminnairi/react-router";
770
+
771
+ createPage({
772
+ path: "/oauth/callback",
773
+ element: function OauthCallback() {
774
+ const token = useHash();
775
+
776
+ return (
777
+ <h1>You token is {token}</h1>
778
+ );
779
+ }
780
+ });
781
+ ```
782
+
783
+ ## Internal API
784
+
526
785
  ### doesRouteMatchPath
527
786
 
528
787
  Return a boolean in case a route matches a path. A route is a URI that looks something like `/users/:user/articles` and a path is the browser's location pathname that looks something like `/users/123/articles`.
@@ -541,6 +800,20 @@ doesRoutePatchPath("/users/:user", "/users/123"); // true
541
800
  doesRoutePatchPath("/users/:user", "/users/123/articles"); // false
542
801
  ```
543
802
 
803
+ You can also optionally provide a prefix.
804
+
805
+ ```typescript
806
+ import { doesRouteMatchPath } from "@aminnairi/react-router";
807
+
808
+ doesRoutePatchPath("/", "/github", "/github"); // true
809
+
810
+ doesRoutePatchPath("/", "/github/about", "/github"); // false
811
+
812
+ doesRoutePatchPath("/users/:user", "/github/users/123", "/github"); // true
813
+
814
+ doesRoutePatchPath("/users/:user", "/github/users/123/articles", "/github"); // false
815
+ ```
816
+
544
817
  ### getParameters
545
818
 
546
819
  Return an object in case a route matches a path, with its dynamic parameters as output. It returns a generic `object` type in case no dynamic parameters are found in the URI. Note that the parameters are always strings, if you need to, convert them to other types explicitely.
@@ -559,6 +832,20 @@ getParameters("/users/:user", "/users/123"); // { user: "123" }
559
832
  getParameters("/users/:user", "/users/123/articles"); // { user: "123" }
560
833
  ```
561
834
 
835
+ You can also provide an optional prefix.
836
+
837
+ ```typescript
838
+ import { getParameters } from "@aminnairi/react-router";
839
+
840
+ getParameters("/", "/github", "/github"); // object
841
+
842
+ getParameters("/", "/github/about", "/github"); // object
843
+
844
+ getParameters("/users/:user", "/github/users/123", "/github"); // { user: "123" }
845
+
846
+ getParameters("/users/:user", "/github/users/123/articles", "/github"); // { user: "123" }
847
+ ```
848
+
562
849
  ### findPage
563
850
 
564
851
  Return a page that matches the `window.location.pathname` property containing the current URI of the page from an array of pages.
@@ -591,10 +878,44 @@ const pages = [
591
878
  login.page
592
879
  ];
593
880
 
594
- const foundPage = findPage({
595
- pages
881
+ const foundPage = findPage(pages, "/login");
882
+
883
+ if (foundPage) {
884
+ console.log("Found a page matching the current location");
885
+ console.log(foundPage.path);
886
+ } else {
887
+ console.log("No page matching the current location.");
888
+ }
889
+ ```
890
+
891
+ You can also provide an optional prefix.
892
+
893
+ ```tsx
894
+ import { findPage, createPage } from "@aminnairi/react-router";
895
+
896
+ const home = createPage({
897
+ path: "/",
898
+ element: () => <h1>Home</h1>
899
+ });
900
+
901
+ const about = createPage({
902
+ path: "/about",
903
+ element: () => <h1>About</h1>
904
+ });
905
+
906
+ const login = createPage({
907
+ path: "/login",
908
+ element: () => <h1>Login</h1>
596
909
  });
597
910
 
911
+ const pages = [
912
+ home.page,
913
+ about.page,
914
+ login.page
915
+ ];
916
+
917
+ const foundPage = findPage(pages, "/github/login", "/github");
918
+
598
919
  if (foundPage) {
599
920
  console.log("Found a page matching the current location");
600
921
  console.log(foundPage.path);
@@ -603,6 +924,22 @@ if (foundPage) {
603
924
  }
604
925
  ```
605
926
 
927
+ ### sanitizePath
928
+
929
+ Internal function that helps normalizing the URL by removing trailing and leading slashes as well as removing any duplicate and unecessary slashes.
930
+
931
+ ```ts
932
+ import { sanitizePath } from "@aminnairi/react-router";
933
+
934
+ sanitizePath("/"); // "/"
935
+
936
+ sanitizePath("users"); // "/users"
937
+
938
+ sanitizePath("users/"); // "/users"
939
+
940
+ sanitizePath("users//123///articles"); // "/users/123/articles"
941
+ ```
942
+
606
943
  ## Features
607
944
 
608
945
  ### TypeScript
@@ -633,4 +970,58 @@ Never fear having a blank page again when a component throws. This library lets
633
970
 
634
971
  ## License
635
972
 
636
- See [`LICENSE`](./LICENSE).
973
+ See [`LICENSE`](./LICENSE).
974
+
975
+ ## Changelogs
976
+
977
+ ### Versions
978
+
979
+ - [`1.0.0`](#100)
980
+ - [`0.1.1`](#011)
981
+ - [`0.1.0`](#010)
982
+
983
+ ### 1.0.0
984
+
985
+ #### Major changes
986
+
987
+ - The arguments of `findPage` have moved from an object to regular arguments, with the first one being the path, and the second being the current route
988
+ - Removed the `page.navigate` property in favor of the new `useNavigateTo` hook
989
+ - The `createPage` now returns the page directly instead of exposing it in an object
990
+
991
+ #### Minor changes
992
+
993
+ - Added a `Provider` component from the created `router` which exposes variables for the children such as the location
994
+ - Added a new `useIsActivePage` hook for the created `router` which helps computing if a given page is active or not
995
+ - Added a new `useSearch` hook for the created `router` for getting search parameters from the current URL
996
+ - Added a new `useHash` hook for the created `router` for getting the URL fragment
997
+ - Added a new `sanitizePath` function for removing unecessary and extra slashes in a given string
998
+ - Added a new `useNavigateTo` hook that replaces the old `page.navigate` function
999
+ - Added a `prefix` property in order to use prefix for routes that need it like GitHub Pages
1000
+
1001
+ ### 0.1.1
1002
+
1003
+ #### Major changes
1004
+
1005
+ None.
1006
+
1007
+ #### Minor changes
1008
+
1009
+ None.
1010
+
1011
+ #### Bug & security fixes
1012
+
1013
+ Fixed peer dependency for react.
1014
+
1015
+ ### 0.1.0
1016
+
1017
+ #### Major changes
1018
+
1019
+ None.
1020
+
1021
+ #### Minor changes
1022
+
1023
+ None.
1024
+
1025
+ #### Bug & security fixes
1026
+
1027
+ None.
package/index.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useEffect, useState, FunctionComponent, useMemo, Component, PropsWithChildren } from "react";
1
+ import { useEffect, useState, FunctionComponent, useMemo, Component, PropsWithChildren, createContext, SetStateAction, Dispatch, ReactNode, useContext } from "react";
2
2
 
3
3
  export type AbsolutePath<Path extends string> =
4
4
  Path extends `${infer Start}:${string}/${infer Rest}`
@@ -25,46 +25,32 @@ export interface Page<Path extends string> {
25
25
  element: FunctionComponent<PageComponentProps<Path>>
26
26
  }
27
27
 
28
- export interface CreateRouterOptions {
28
+ export interface CreateRouterOptions<Path extends string> {
29
29
  transition?: boolean,
30
- pages: Array<Page<string>>
30
+ prefix?: string,
31
+ pages: Array<Page<Path>>
31
32
  fallback: FunctionComponent
32
33
  issue: FunctionComponent<IssueProps>
33
34
  }
34
35
 
35
- export interface CreateRouteOutput<Path extends string> {
36
- page: Page<Path>,
37
- navigate: (parameters: Parameters<Path>) => void
38
- }
39
-
40
36
  export interface FindPageOptions {
41
37
  pages: Array<Page<string>>
38
+ path: string
42
39
  }
43
40
 
44
- export const createPage = <Path extends string>(page: Page<Path>): CreateRouteOutput<Path> => {
45
- const navigate = (parameters: Parameters<Path>, replace: boolean = false) => {
46
- const pathWithParameters = Object.entries(parameters).reduce((path, [parameterName, parameterValue]) => {
47
- return path.replace(`:${parameterName}`, parameterValue);
48
- }, page.path as string);
49
-
50
- if (replace) {
51
- window.history.replaceState(null, pathWithParameters, pathWithParameters);
52
- } else {
53
- window.history.pushState(null, pathWithParameters, pathWithParameters);
54
- }
55
-
56
- window.dispatchEvent(new CustomEvent("popstate"));
57
- };
41
+ export const sanitizePath = (path: string): string => {
42
+ return "/" + path
43
+ .replace(/^\/|\/$/g, "")
44
+ .replace(/\/+/g, "/");
45
+ }
58
46
 
59
- return {
60
- page,
61
- navigate
62
- };
47
+ export const createPage = <Path extends string>(page: Page<Path>) => {
48
+ return page
63
49
  }
64
50
 
65
- export const doesRouteMatchPath = (path: string, route: string): boolean => {
66
- const pathParts = path.split("/").filter(Boolean);
67
- const routeParts = route.split("/").filter(Boolean);
51
+ export const doesRouteMatchPath = (path: string, route: string, prefix?: string): boolean => {
52
+ const pathParts = sanitizePath(`${prefix ?? ""}/${path}`).split("/").filter(Boolean);
53
+ const routeParts = sanitizePath(route).split("/").filter(Boolean);
68
54
 
69
55
  return (
70
56
  pathParts.length === routeParts.length &&
@@ -72,18 +58,35 @@ export const doesRouteMatchPath = (path: string, route: string): boolean => {
72
58
  );
73
59
  }
74
60
 
75
- export const getParameters = <Path extends string>(config: { path: Path; route: string }): Parameters<Path> => {
76
- return Object.fromEntries(
77
- config.path
78
- .split("/")
79
- .map((part, index) => (part.startsWith(":") ? [part.slice(1), config.route.split("/")[index]] : null))
80
- .filter((entry): entry is [string, string] => entry !== null)
81
- ) as Parameters<Path>;
61
+ export const getParameters = <Path extends string>(path: Path, route: string, prefix?: string): Parameters<Path> => {
62
+ if (!doesRouteMatchPath(path, route, prefix)) {
63
+ return {} as Parameters<Path>;
64
+ }
65
+
66
+ const pathParts = sanitizePath(`${prefix ?? ""}/${path}`).split("/").filter(Boolean);
67
+ const routeParts = sanitizePath(route).split("/").filter(Boolean);
68
+
69
+ return pathParts.reduce((parameters, pathPart, pathPartIndex) => {
70
+ const routePart = routeParts[pathPartIndex];
71
+
72
+ if (!routePart) {
73
+ return parameters;
74
+ }
75
+
76
+ if (!pathPart.startsWith(":")) {
77
+ return parameters;
78
+ }
79
+
80
+ return {
81
+ ...parameters,
82
+ [`${pathPart.slice(1)}`]: routePart
83
+ }
84
+ }, {} as Parameters<Path>);
82
85
  }
83
86
 
84
- const findPage = ({ pages }: FindPageOptions) => {
87
+ const findPage = (pages: Array<Page<string>>, path: string, prefix?: string) => {
85
88
  const foundPage = pages.find(route => {
86
- return doesRouteMatchPath(route.path, window.location.pathname);
89
+ return doesRouteMatchPath(sanitizePath(`${prefix ?? ""}/${route.path}`), sanitizePath(path));
87
90
  });
88
91
 
89
92
  return foundPage;
@@ -150,34 +153,95 @@ export const createIssue = (issue: FunctionComponent<IssueProps>) => {
150
153
  return issue;
151
154
  }
152
155
 
153
- export const createRouter = ({ pages, fallback, transition: withViewTransition, issue }: CreateRouterOptions) => {
154
- const View = () => {
155
- const [page, setPage] = useState(findPage({ pages }));
156
- const shouldTransitionBetweenPages = useMemo(() => typeof document.startViewTransition === "function" && withViewTransition ? true : false, [withViewTransition]);
157
- const Fallback = useMemo(() => fallback, []);
156
+ export interface ContextInterface {
157
+ prefix: string,
158
+ pathname: string,
159
+ setPathname: Dispatch<SetStateAction<string>>,
160
+ search: URLSearchParams,
161
+ setSearch: Dispatch<SetStateAction<URLSearchParams>>,
162
+ hash: string,
163
+ setHash: Dispatch<SetStateAction<string>>
164
+ }
158
165
 
159
- const parameters = useMemo(() => {
160
- if (page) {
161
- return getParameters({
162
- path: page.path,
163
- route: window.location.pathname
164
- })
165
- }
166
+ export interface ProviderProps {
167
+ children: ReactNode
168
+ }
166
169
 
167
- return {};
168
- }, [page]);
170
+ const Context = createContext<ContextInterface>({
171
+ prefix: "",
172
+ pathname: sanitizePath(window.location.pathname),
173
+ setPathname: () => { },
174
+ search: new URLSearchParams(),
175
+ setSearch: () => { },
176
+ hash: window.location.hash,
177
+ setHash: () => { }
178
+ });
179
+
180
+ export const useNavigateToPage = <Path extends string>(page: Page<Path>) => {
181
+ const { prefix } = useContext(Context);
182
+
183
+ return (parameters: Parameters<Path>, replace: boolean = false) => {
184
+ const pathWithParameters = Object.entries(parameters).reduce((path, [parameterName, parameterValue]) => {
185
+ return path.replace(`:${parameterName}`, parameterValue);
186
+ }, sanitizePath(`${prefix ?? ""}/${page.path}`));
187
+
188
+ if (replace) {
189
+ window.history.replaceState(null, pathWithParameters, pathWithParameters);
190
+ } else {
191
+ window.history.pushState(null, pathWithParameters, pathWithParameters);
192
+ }
193
+
194
+ window.dispatchEvent(new CustomEvent("popstate"));
195
+ }
196
+ };
197
+
198
+ export const useIsActivePage = (page: Page<string>) => {
199
+ const { pathname, prefix } = useContext(Context);
200
+
201
+ return doesRouteMatchPath(sanitizePath(page.path), sanitizePath(pathname), prefix);
202
+ };
203
+
204
+ export const useSearch = () => {
205
+ const { search } = useContext(Context);
206
+
207
+ return search;
208
+ };
209
+
210
+ export const useHash = () => {
211
+ const { hash } = useContext(Context);
212
+ return hash;
213
+ };
214
+
215
+ export const createRouter = <Path extends string>({ pages, fallback, transition: withViewTransition, issue, prefix }: CreateRouterOptions<Path>) => {
216
+ const Provider = ({ children }: ProviderProps) => {
217
+ const [pathname, setPathname] = useState(sanitizePath(window.location.pathname));
218
+ const [search, setSearch] = useState(new URLSearchParams(sanitizePath(window.location.search)));
219
+ const [hash, setHash] = useState(window.location.hash);
220
+ const shouldTransitionBetweenPages = useMemo(() => typeof document.startViewTransition === "function" && withViewTransition ? true : false, [withViewTransition]);
221
+
222
+ const value = useMemo(() => {
223
+ return {
224
+ prefix: prefix ?? "",
225
+ pathname,
226
+ search,
227
+ hash,
228
+ setPathname,
229
+ setSearch,
230
+ setHash
231
+ };
232
+ }, [prefix, pathname, search, hash]);
169
233
 
170
234
  useEffect(() => {
171
235
  const onWindowPopstate = () => {
172
- const foundPage = findPage({ pages });
173
-
174
236
  if (shouldTransitionBetweenPages) {
175
237
  document.startViewTransition(() => {
176
- setPage(foundPage);
238
+ setPathname(sanitizePath(window.location.pathname));
177
239
  });
178
- } else {
179
- setPage(foundPage);
240
+
241
+ return;
180
242
  }
243
+
244
+ setPathname(sanitizePath(window.location.pathname));
181
245
  };
182
246
 
183
247
  window.addEventListener("popstate", onWindowPopstate);
@@ -187,18 +251,41 @@ export const createRouter = ({ pages, fallback, transition: withViewTransition,
187
251
  }
188
252
  }, []);
189
253
 
190
- if (page) {
191
- return (
254
+ return (
255
+ <Context.Provider value={value}>
192
256
  <ErrorBoundary fallback={issue} transition={shouldTransitionBetweenPages}>
193
- {<page.element parameters={parameters} />}
257
+ {children}
194
258
  </ErrorBoundary>
259
+ </Context.Provider>
260
+ );
261
+ };
262
+
263
+ const View = () => {
264
+ const Fallback = useMemo(() => fallback, []);
265
+ const { pathname } = useContext(Context);
266
+ const page = useMemo(() => findPage(pages, pathname, prefix), [pathname]);
267
+
268
+ const parameters = useMemo(() => {
269
+ if (page) {
270
+ return getParameters(sanitizePath(page.path), sanitizePath(window.location.pathname), prefix);
271
+ }
272
+
273
+ return {};
274
+ }, [page]);
275
+
276
+ if (page) {
277
+ return (
278
+ <page.element parameters={parameters} />
195
279
  );
196
280
  }
197
281
 
198
- return <Fallback />;
282
+ return (
283
+ <Fallback />
284
+ );
199
285
  };
200
286
 
201
287
  return {
202
- View
288
+ View,
289
+ Provider,
203
290
  };
204
291
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "type": "module",
3
3
  "name": "@aminnairi/react-router",
4
4
  "description": "Type-safe router for the React library",
5
- "version": "0.1.0",
5
+ "version": "1.0.0",
6
6
  "homepage": "https://github.com/aminnairi/react-router#readme",
7
7
  "license": "MIT",
8
8
  "bugs": {
@@ -20,9 +20,11 @@
20
20
  "router",
21
21
  "hook",
22
22
  "typescript",
23
- "transition"
23
+ "transition",
24
+ "navigation",
25
+ "history"
24
26
  ],
25
27
  "peerDependencies": {
26
- "react": "^18.0.0"
28
+ "react": ">=18.0.0"
27
29
  }
28
30
  }