@aminnairi/react-router 0.1.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/LICENSE +7 -0
- package/README.md +636 -0
- package/index.tsx +204 -0
- package/package.json +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025 Amin NAIRI
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
# @aminnairi/react-router
|
|
2
|
+
|
|
3
|
+
Type-safe router for the React library
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- [Node](https://nodejs.org/)
|
|
8
|
+
- [NPM](https://npmjs.com/)
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
### Project initialization
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm create vite -- --template react-ts project
|
|
16
|
+
cd project
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Dependencies installation
|
|
20
|
+
```bash
|
|
21
|
+
npm install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Library installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @aminnairi/react-router
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Setup
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
mkdir src/router
|
|
34
|
+
mkdir src/router/pages
|
|
35
|
+
touch src/router/pages/home.tsx
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { createPage } from "@aminnairi/react-router";
|
|
40
|
+
|
|
41
|
+
export const home = createPage({
|
|
42
|
+
path: "/",
|
|
43
|
+
element: () => <h1>Home page</h1>
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
touch src/router/fallback.tsx
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { home } from "./pages/home";
|
|
53
|
+
|
|
54
|
+
export const Fallback = () => {
|
|
55
|
+
return (
|
|
56
|
+
<button onClick={home.navigate}>
|
|
57
|
+
Go back home
|
|
58
|
+
</button>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
touch src/router/issue.tsx
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { Fragment } from "react";
|
|
69
|
+
import { home } from "./pages/home";
|
|
70
|
+
|
|
71
|
+
export const Issue = () => {
|
|
72
|
+
return (
|
|
73
|
+
<Fragment>
|
|
74
|
+
<h1>An issue occurred</h1>
|
|
75
|
+
<button onClick={home.navigate}>
|
|
76
|
+
Go back home
|
|
77
|
+
</button>
|
|
78
|
+
</Fragment>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
touch src/router/index.ts
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import { createRouter } from "@aminnairi/react-router";
|
|
89
|
+
import { Fallback } from "./router/fallback";
|
|
90
|
+
import { Issue } from "./router/issue";
|
|
91
|
+
import { home } from "./router/pages/home";
|
|
92
|
+
|
|
93
|
+
export const router = createRouter({
|
|
94
|
+
fallback: Fallback,
|
|
95
|
+
issue: Issue,
|
|
96
|
+
routes: [
|
|
97
|
+
home.page
|
|
98
|
+
]
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
touch src/App.tsx
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { router } from "./router";
|
|
108
|
+
|
|
109
|
+
export default function App() {
|
|
110
|
+
return (
|
|
111
|
+
<router.View />
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Startup
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npm run dev
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## API
|
|
123
|
+
|
|
124
|
+
### createPage
|
|
125
|
+
|
|
126
|
+
Creates a new page definition that can then later be used to create a router. It takes the path of the page to create as well as the element that needs to be rendered when a client navigates to this page.
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
import { createPage } from "@aminnairi/react-router";
|
|
130
|
+
|
|
131
|
+
createPage({
|
|
132
|
+
path: "/",
|
|
133
|
+
element: () => (
|
|
134
|
+
<h1>Home</h1>
|
|
135
|
+
)
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
You can then inject the page inside a router.
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
import { createPage, createRouter } from "@aminnairi/react-router";
|
|
143
|
+
|
|
144
|
+
const home = createPage({
|
|
145
|
+
path: "/",
|
|
146
|
+
element: () => (
|
|
147
|
+
<h1>
|
|
148
|
+
Home
|
|
149
|
+
</h1>
|
|
150
|
+
)
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
createRouter({
|
|
154
|
+
fallback: () => (
|
|
155
|
+
<h1>
|
|
156
|
+
Not found
|
|
157
|
+
</h1>
|
|
158
|
+
),
|
|
159
|
+
issue: () => (
|
|
160
|
+
<h1>
|
|
161
|
+
An error occurred
|
|
162
|
+
</h1>
|
|
163
|
+
),
|
|
164
|
+
pages: [
|
|
165
|
+
home.page
|
|
166
|
+
]
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
You can define a page that has dynamic parameters, and get back into the element the needed parameters.
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
import { createPage } from "@aminnairi/react-router";
|
|
174
|
+
|
|
175
|
+
createPage({
|
|
176
|
+
path: "/users/:user",
|
|
177
|
+
element: ({ parameters: { user }}) => (
|
|
178
|
+
<h1>
|
|
179
|
+
User#{user}
|
|
180
|
+
</h1>
|
|
181
|
+
)
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
And if you can have of course more than one dynamic parameter.
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
import { createPage } from "@aminnairi/react-router";
|
|
189
|
+
|
|
190
|
+
createPage({
|
|
191
|
+
path: "/users/:user/articles/:article",
|
|
192
|
+
element: ({ parameters: { user, article }}) => (
|
|
193
|
+
<h1>
|
|
194
|
+
Article#{article } of user#{user}
|
|
195
|
+
</h1>
|
|
196
|
+
)
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
You can also navigate to one page from another.
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
import { Fragment } from "react";
|
|
204
|
+
import { createPage } from "@aminnairi/react-router";
|
|
205
|
+
|
|
206
|
+
const login = createPage({
|
|
207
|
+
path: "/login",
|
|
208
|
+
element: () => (
|
|
209
|
+
<h1>
|
|
210
|
+
Login
|
|
211
|
+
</h1>
|
|
212
|
+
)
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const about = createPage({
|
|
216
|
+
path: "/about",
|
|
217
|
+
element: () => (
|
|
218
|
+
<Fragment>
|
|
219
|
+
<h1>
|
|
220
|
+
About Us
|
|
221
|
+
</h1>
|
|
222
|
+
<button onClick={() => login.navigate({})}>
|
|
223
|
+
</button>
|
|
224
|
+
</Fragment>
|
|
225
|
+
)
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
createPage({
|
|
229
|
+
path: "/",
|
|
230
|
+
element: () => (
|
|
231
|
+
<Fragment>
|
|
232
|
+
<h1>
|
|
233
|
+
Home
|
|
234
|
+
</h1>
|
|
235
|
+
<button onClick={about.navigate}>
|
|
236
|
+
About Us
|
|
237
|
+
</button>
|
|
238
|
+
</Fragment>
|
|
239
|
+
)
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
And you can of course navigate to pages that have dynamic parameters as well.
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
import { Fragment } from "react";
|
|
247
|
+
import { createPage } from "@aminnairi/react-router";
|
|
248
|
+
|
|
249
|
+
const user = createPage({
|
|
250
|
+
path: "/users/:user",
|
|
251
|
+
element: ({ parameters: { user }}) => (
|
|
252
|
+
<h1>
|
|
253
|
+
User#{user}
|
|
254
|
+
</h1>
|
|
255
|
+
)
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
createPage({
|
|
259
|
+
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
|
+
)
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### createRouter
|
|
274
|
+
|
|
275
|
+
Creates a router that you can then use to display the view, which is the page matching the current browser's location.
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
import { Fragment, StrictMode } from "react";
|
|
279
|
+
import { createRoot } from "react-dom/client";
|
|
280
|
+
import { createRouter, createPage } from "@aminnairi/react-router";
|
|
281
|
+
|
|
282
|
+
const home = createPage({
|
|
283
|
+
path: "/",
|
|
284
|
+
element: () => (
|
|
285
|
+
<h1>Home</h1>
|
|
286
|
+
)
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const router = createRouter({
|
|
290
|
+
fallback: () => (
|
|
291
|
+
<h1>Not found</h1>
|
|
292
|
+
),
|
|
293
|
+
issue: () => (
|
|
294
|
+
<h1>An error occurred</h1>
|
|
295
|
+
),
|
|
296
|
+
pages: [
|
|
297
|
+
home.page
|
|
298
|
+
]
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const rootElement = document.getElementById("root");
|
|
302
|
+
|
|
303
|
+
if (!rootElement) {
|
|
304
|
+
throw new Error("Root element not found");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const root = createRoot(rootElement);
|
|
308
|
+
|
|
309
|
+
const App = () => {
|
|
310
|
+
return (
|
|
311
|
+
<Fragment>
|
|
312
|
+
<header>
|
|
313
|
+
<h1>App</h1>
|
|
314
|
+
</header>
|
|
315
|
+
<main>
|
|
316
|
+
<router.View />
|
|
317
|
+
</main>
|
|
318
|
+
<footer>
|
|
319
|
+
Credit © Yourself 2025
|
|
320
|
+
</footer>
|
|
321
|
+
</Fragment>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
root.render(
|
|
326
|
+
<StrictMode>
|
|
327
|
+
<App />
|
|
328
|
+
</StrictMode>
|
|
329
|
+
);
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
You can also activate the View Transition Web API if you want before each page renders. This is nice because by default, the browser already has some styling that allows for a smooth and simple transition between pages.
|
|
333
|
+
|
|
334
|
+
All you have to do is to set the `withViewTransition` property to `true` in the arguments of the `createRouter` function. By default, its value is set to `false` if not provided in the arguments of the `createRouter` function.
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
import { Fragment, StrictMode } from "react";
|
|
338
|
+
import { createRoot } from "react-dom/client";
|
|
339
|
+
import { createRouter, createPage } from "@aminnairi/react-router";
|
|
340
|
+
|
|
341
|
+
const home = createPage({
|
|
342
|
+
path: "/",
|
|
343
|
+
element: () => (
|
|
344
|
+
<h1>Home</h1>
|
|
345
|
+
)
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const router = createRouter({
|
|
349
|
+
transition: true,
|
|
350
|
+
fallback: () => (
|
|
351
|
+
<h1>Not found</h1>
|
|
352
|
+
),
|
|
353
|
+
issue: () => (
|
|
354
|
+
<h1>An error occurred</h1>
|
|
355
|
+
),
|
|
356
|
+
pages: [
|
|
357
|
+
home.page
|
|
358
|
+
]
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const rootElement = document.getElementById("root");
|
|
362
|
+
|
|
363
|
+
if (!rootElement) {
|
|
364
|
+
throw new Error("Root element not found");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const root = createRoot(rootElement);
|
|
368
|
+
|
|
369
|
+
const App = () => {
|
|
370
|
+
return (
|
|
371
|
+
<Fragment>
|
|
372
|
+
<header>
|
|
373
|
+
<h1>App</h1>
|
|
374
|
+
</header>
|
|
375
|
+
<main>
|
|
376
|
+
<router.View />
|
|
377
|
+
</main>
|
|
378
|
+
<footer>
|
|
379
|
+
Credit © Yourself 2025
|
|
380
|
+
</footer>
|
|
381
|
+
</Fragment>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
root.render(
|
|
386
|
+
<StrictMode>
|
|
387
|
+
<App />
|
|
388
|
+
</StrictMode>
|
|
389
|
+
);
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
The `createRouter` takes a functional component that allow you to react to error in case a component throws. You can use the props to get a property `error` containing the error that has been thrown as well as a `reset` function that allow you to reset the error.
|
|
393
|
+
|
|
394
|
+
```tsx
|
|
395
|
+
import { Fragment, StrictMode } from "react";
|
|
396
|
+
import { createRoot } from "react-dom/client";
|
|
397
|
+
import { createRouter, createPage } from "@aminnairi/react-router";
|
|
398
|
+
|
|
399
|
+
const home = createPage({
|
|
400
|
+
path: "/",
|
|
401
|
+
element: () => (
|
|
402
|
+
<h1>Home</h1>
|
|
403
|
+
)
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const router = createRouter({
|
|
407
|
+
transition: true,
|
|
408
|
+
fallback: () => (
|
|
409
|
+
<h1>Not found</h1>
|
|
410
|
+
),
|
|
411
|
+
issue: ({ error, reset }) => (
|
|
412
|
+
return (
|
|
413
|
+
<Fragment>
|
|
414
|
+
<h1>Error</h1>
|
|
415
|
+
<p>{error.message}</p>
|
|
416
|
+
<button onClick={reset}>Reset</button>
|
|
417
|
+
</Fragment>
|
|
418
|
+
);
|
|
419
|
+
),
|
|
420
|
+
pages: [
|
|
421
|
+
home.page
|
|
422
|
+
]
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const rootElement = document.getElementById("root");
|
|
426
|
+
|
|
427
|
+
if (!rootElement) {
|
|
428
|
+
throw new Error("Root element not found");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const root = createRoot(rootElement);
|
|
432
|
+
|
|
433
|
+
const App = () => {
|
|
434
|
+
return (
|
|
435
|
+
<Fragment>
|
|
436
|
+
<header>
|
|
437
|
+
<h1>App</h1>
|
|
438
|
+
</header>
|
|
439
|
+
<main>
|
|
440
|
+
<router.View />
|
|
441
|
+
</main>
|
|
442
|
+
<footer>
|
|
443
|
+
Credit © Yourself 2025
|
|
444
|
+
</footer>
|
|
445
|
+
</Fragment>
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
root.render(
|
|
450
|
+
<StrictMode>
|
|
451
|
+
<App />
|
|
452
|
+
</StrictMode>
|
|
453
|
+
);
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
You can also define this function from the outside by using the `createIssue` function.
|
|
457
|
+
|
|
458
|
+
```tsx
|
|
459
|
+
import { Fragment, StrictMode } from "react";
|
|
460
|
+
import { createRoot } from "react-dom/client";
|
|
461
|
+
import { createRouter, createPage, createIssue } from "@aminnairi/react-router";
|
|
462
|
+
|
|
463
|
+
const home = createPage({
|
|
464
|
+
path: "/",
|
|
465
|
+
element: () => (
|
|
466
|
+
<h1>Home</h1>
|
|
467
|
+
)
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const Fallback = () => {
|
|
471
|
+
return (
|
|
472
|
+
<h1>Not found</h1>
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const Issue = createIssue(({ error, reset }) => (
|
|
477
|
+
return (
|
|
478
|
+
<Fragment>
|
|
479
|
+
<h1>Error</h1>
|
|
480
|
+
<p>{error.message}</p>
|
|
481
|
+
<button onClick={reset}>Reset</button>
|
|
482
|
+
</Fragment>
|
|
483
|
+
);
|
|
484
|
+
));
|
|
485
|
+
|
|
486
|
+
const router = createRouter({
|
|
487
|
+
transition: true,
|
|
488
|
+
fallback: Fallback,
|
|
489
|
+
issue: Issue,
|
|
490
|
+
pages: [
|
|
491
|
+
home.page
|
|
492
|
+
]
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const rootElement = document.getElementById("root");
|
|
496
|
+
|
|
497
|
+
if (!rootElement) {
|
|
498
|
+
throw new Error("Root element not found");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const root = createRoot(rootElement);
|
|
502
|
+
|
|
503
|
+
const App = () => {
|
|
504
|
+
return (
|
|
505
|
+
<Fragment>
|
|
506
|
+
<header>
|
|
507
|
+
<h1>App</h1>
|
|
508
|
+
</header>
|
|
509
|
+
<main>
|
|
510
|
+
<router.View />
|
|
511
|
+
</main>
|
|
512
|
+
<footer>
|
|
513
|
+
Credit © Yourself 2025
|
|
514
|
+
</footer>
|
|
515
|
+
</Fragment>
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
root.render(
|
|
520
|
+
<StrictMode>
|
|
521
|
+
<App />
|
|
522
|
+
</StrictMode>
|
|
523
|
+
);
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### doesRouteMatchPath
|
|
527
|
+
|
|
528
|
+
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`.
|
|
529
|
+
|
|
530
|
+
This function is mainly used in the internals of the `createRouter` and in most case should not be necessary.
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
import { doesRouteMatchPath } from "@aminnairi/react-router";
|
|
534
|
+
|
|
535
|
+
doesRoutePatchPath("/", "/"); // true
|
|
536
|
+
|
|
537
|
+
doesRoutePatchPath("/", "/about"); // false
|
|
538
|
+
|
|
539
|
+
doesRoutePatchPath("/users/:user", "/users/123"); // true
|
|
540
|
+
|
|
541
|
+
doesRoutePatchPath("/users/:user", "/users/123/articles"); // false
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### getParameters
|
|
545
|
+
|
|
546
|
+
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.
|
|
547
|
+
|
|
548
|
+
This function is mainly used in the internals of the `createRouter` and in most case should not be necessary.
|
|
549
|
+
|
|
550
|
+
```typescript
|
|
551
|
+
import { getParameters } from "@aminnairi/react-router";
|
|
552
|
+
|
|
553
|
+
getParameters("/", "/"); // object
|
|
554
|
+
|
|
555
|
+
getParameters("/", "/about"); // object
|
|
556
|
+
|
|
557
|
+
getParameters("/users/:user", "/users/123"); // { user: "123" }
|
|
558
|
+
|
|
559
|
+
getParameters("/users/:user", "/users/123/articles"); // { user: "123" }
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### findPage
|
|
563
|
+
|
|
564
|
+
Return a page that matches the `window.location.pathname` property containing the current URI of the page from an array of pages.
|
|
565
|
+
|
|
566
|
+
If it does not match any pages, it returns `undefined` instead.
|
|
567
|
+
|
|
568
|
+
This function is mainly used in the internals of the `createRouter` and in most case should not be necessary.
|
|
569
|
+
|
|
570
|
+
```tsx
|
|
571
|
+
import { findPage, createPage } from "@aminnairi/react-router";
|
|
572
|
+
|
|
573
|
+
const home = createPage({
|
|
574
|
+
path: "/",
|
|
575
|
+
element: () => <h1>Home</h1>
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const about = createPage({
|
|
579
|
+
path: "/about",
|
|
580
|
+
element: () => <h1>About</h1>
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
const login = createPage({
|
|
584
|
+
path: "/login",
|
|
585
|
+
element: () => <h1>Login</h1>
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
const pages = [
|
|
589
|
+
home.page,
|
|
590
|
+
about.page,
|
|
591
|
+
login.page
|
|
592
|
+
];
|
|
593
|
+
|
|
594
|
+
const foundPage = findPage({
|
|
595
|
+
pages
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
if (foundPage) {
|
|
599
|
+
console.log("Found a page matching the current location");
|
|
600
|
+
console.log(foundPage.path);
|
|
601
|
+
} else {
|
|
602
|
+
console.log("No page matching the current location.");
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
## Features
|
|
607
|
+
|
|
608
|
+
### TypeScript
|
|
609
|
+
|
|
610
|
+
This library has been written in TypeScript from the ground up, no manual definition types created, only pure TypeScript.
|
|
611
|
+
|
|
612
|
+
Type-safety has been the #1 goal, this means that you can fearlessly refactor your code without forgetting to update one part of your code that might break, types got you covered.
|
|
613
|
+
|
|
614
|
+
### No codegen
|
|
615
|
+
|
|
616
|
+
Code generation is useful in environment where multiple languages may be used, but in the case of a Web application written in TypeScript, there is no need for any codegen at all, thus reducing the surface of errors possibly generated by such tools, and greatly reducing complexity when setting up a router.
|
|
617
|
+
|
|
618
|
+
### Simplicity
|
|
619
|
+
|
|
620
|
+
This library does nothing more other than abstracting for your the complexity of using the History Web API, as well as providing you with type safety out of the box.
|
|
621
|
+
|
|
622
|
+
This means that you can use this library with other popular solutions for handling metadata for instance.
|
|
623
|
+
|
|
624
|
+
### Transition
|
|
625
|
+
|
|
626
|
+
Support for the View Transition API is built-in and allows for painless and smooth view transition out-of-the-box without having to do anything.
|
|
627
|
+
|
|
628
|
+
This can also easily be disabled if needed.
|
|
629
|
+
|
|
630
|
+
### Error handling
|
|
631
|
+
|
|
632
|
+
Never fear having a blank page again when a component throws. This library lets you define a functional component that will answer to any error that might be raised by any pages so that you can react accordingly by providing a nice and friendly error page instead of a blank or white page.
|
|
633
|
+
|
|
634
|
+
## License
|
|
635
|
+
|
|
636
|
+
See [`LICENSE`](./LICENSE).
|
package/index.tsx
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { useEffect, useState, FunctionComponent, useMemo, Component, PropsWithChildren } from "react";
|
|
2
|
+
|
|
3
|
+
export type AbsolutePath<Path extends string> =
|
|
4
|
+
Path extends `${infer Start}:${string}/${infer Rest}`
|
|
5
|
+
? `${Start}${string}/${AbsolutePath<Rest>}`
|
|
6
|
+
: Path extends `${infer Start}:${string}`
|
|
7
|
+
? `${Start}${string}`
|
|
8
|
+
: Path;
|
|
9
|
+
|
|
10
|
+
export type Parameters<Path extends string> =
|
|
11
|
+
Path extends `${string}/:${infer Segment}/${infer Rest}`
|
|
12
|
+
? { [K in Segment]: string } & Parameters<`/${Rest}`>
|
|
13
|
+
: Path extends `${string}/:${infer Segment}`
|
|
14
|
+
? { [K in Segment]: string }
|
|
15
|
+
: object;
|
|
16
|
+
|
|
17
|
+
export type GoToPageFunction<Path extends string> = (path: AbsolutePath<Path>) => void;
|
|
18
|
+
|
|
19
|
+
export interface PageComponentProps<Path extends string> {
|
|
20
|
+
parameters: Parameters<Path>,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Page<Path extends string> {
|
|
24
|
+
path: Path,
|
|
25
|
+
element: FunctionComponent<PageComponentProps<Path>>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CreateRouterOptions {
|
|
29
|
+
transition?: boolean,
|
|
30
|
+
pages: Array<Page<string>>
|
|
31
|
+
fallback: FunctionComponent
|
|
32
|
+
issue: FunctionComponent<IssueProps>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CreateRouteOutput<Path extends string> {
|
|
36
|
+
page: Page<Path>,
|
|
37
|
+
navigate: (parameters: Parameters<Path>) => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface FindPageOptions {
|
|
41
|
+
pages: Array<Page<string>>
|
|
42
|
+
}
|
|
43
|
+
|
|
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
|
+
};
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
page,
|
|
61
|
+
navigate
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const doesRouteMatchPath = (path: string, route: string): boolean => {
|
|
66
|
+
const pathParts = path.split("/").filter(Boolean);
|
|
67
|
+
const routeParts = route.split("/").filter(Boolean);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
pathParts.length === routeParts.length &&
|
|
71
|
+
pathParts.every((part, index) => part.startsWith(":") || part === routeParts[index])
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
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>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const findPage = ({ pages }: FindPageOptions) => {
|
|
85
|
+
const foundPage = pages.find(route => {
|
|
86
|
+
return doesRouteMatchPath(route.path, window.location.pathname);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return foundPage;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export interface IssueProps {
|
|
93
|
+
error: Error,
|
|
94
|
+
reset: () => void,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ErrorBoundaryProps {
|
|
98
|
+
fallback: FunctionComponent<IssueProps>,
|
|
99
|
+
transition: boolean
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ErrorBoundaryState {
|
|
103
|
+
error: Error | null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class ErrorBoundary extends Component<PropsWithChildren<ErrorBoundaryProps>, ErrorBoundaryState> {
|
|
107
|
+
constructor(props: ErrorBoundaryProps) {
|
|
108
|
+
super(props);
|
|
109
|
+
|
|
110
|
+
this.state = { error: null };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static getDerivedStateFromError(error: unknown) {
|
|
114
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
115
|
+
|
|
116
|
+
return { error: normalizedError };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
render() {
|
|
120
|
+
const viewTransitionSupported = typeof document.startViewTransition === "function";
|
|
121
|
+
|
|
122
|
+
if (this.state.error) {
|
|
123
|
+
const reset = () => {
|
|
124
|
+
if (this.props.transition && viewTransitionSupported) {
|
|
125
|
+
document.startViewTransition(() => {
|
|
126
|
+
this.setState({
|
|
127
|
+
error: null
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.setState({
|
|
135
|
+
error: null
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return this.props.fallback({
|
|
140
|
+
error: this.state.error,
|
|
141
|
+
reset
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return this.props.children;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const createIssue = (issue: FunctionComponent<IssueProps>) => {
|
|
150
|
+
return issue;
|
|
151
|
+
}
|
|
152
|
+
|
|
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, []);
|
|
158
|
+
|
|
159
|
+
const parameters = useMemo(() => {
|
|
160
|
+
if (page) {
|
|
161
|
+
return getParameters({
|
|
162
|
+
path: page.path,
|
|
163
|
+
route: window.location.pathname
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {};
|
|
168
|
+
}, [page]);
|
|
169
|
+
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
const onWindowPopstate = () => {
|
|
172
|
+
const foundPage = findPage({ pages });
|
|
173
|
+
|
|
174
|
+
if (shouldTransitionBetweenPages) {
|
|
175
|
+
document.startViewTransition(() => {
|
|
176
|
+
setPage(foundPage);
|
|
177
|
+
});
|
|
178
|
+
} else {
|
|
179
|
+
setPage(foundPage);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
window.addEventListener("popstate", onWindowPopstate);
|
|
184
|
+
|
|
185
|
+
return () => {
|
|
186
|
+
window.removeEventListener("popstate", onWindowPopstate);
|
|
187
|
+
}
|
|
188
|
+
}, []);
|
|
189
|
+
|
|
190
|
+
if (page) {
|
|
191
|
+
return (
|
|
192
|
+
<ErrorBoundary fallback={issue} transition={shouldTransitionBetweenPages}>
|
|
193
|
+
{<page.element parameters={parameters} />}
|
|
194
|
+
</ErrorBoundary>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return <Fallback />;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
View
|
|
203
|
+
};
|
|
204
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "module",
|
|
3
|
+
"name": "@aminnairi/react-router",
|
|
4
|
+
"description": "Type-safe router for the React library",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"homepage": "https://github.com/aminnairi/react-router#readme",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/aminnairi/react-router/issues"
|
|
10
|
+
},
|
|
11
|
+
"author": {
|
|
12
|
+
"name": "Amin NAIRI",
|
|
13
|
+
"url": "https://github.com/aminnairi"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"url": "git+https://github.com/aminnairi/react-router.git"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"react",
|
|
20
|
+
"router",
|
|
21
|
+
"hook",
|
|
22
|
+
"typescript",
|
|
23
|
+
"transition"
|
|
24
|
+
],
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": "^18.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|