Skip to content

Commit 7f995d8

Browse files
authored
feat: Basic router implementation added (#211)
Also router related React utils added
1 parent 60d05b2 commit 7f995d8

15 files changed

Lines changed: 544 additions & 0 deletions

docs/docs/misc/wait.mdx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,23 @@ wait().then(() => {
2323
console.log('Timeout passed');
2424
});
2525
```
26+
27+
### Aborting
28+
29+
You can pass _abort signal_ from [AbortController](https://developer.mozilla.org/ru/docs/Web/API/AbortController) to reject promise:
30+
31+
```js
32+
import { wait } from '@krutoo/utils';
33+
34+
const controller = new AbortController();
35+
36+
wait(1000, { signal: controller.signal })
37+
.then(() => {
38+
// ...
39+
})
40+
.catch(reason => {
41+
console.log(reason); // "Fake reason"
42+
});
43+
44+
controller.abort('Fake reason');
45+
```

docs/docs/react/use-location.mdx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export const meta = {
2+
category: 'React',
3+
title: 'useLocation',
4+
};
5+
6+
# `useLocation`
7+
8+
React hook of state of current location provided by router.
9+
10+
### Usage
11+
12+
```tsx
13+
import { useLocation } from '@krutoo/utils/react';
14+
15+
function ProfilePage() {
16+
const { pathname, hash, search } = useLocation();
17+
18+
return <>{/* ... */}</>;
19+
}
20+
```
21+
22+
### Requirements
23+
24+
You need to wrap your root component to special `RouterContext` to make router specific hooks working:
25+
26+
```tsx
27+
import { createRoot } from 'react-dom';
28+
import { BrowserRouter } from '@krutoo/utils/router';
29+
import { RouterContext } from '@krutoo/utils/react';
30+
import { App } from '#components/app';
31+
32+
const router = new BrowserRouter();
33+
34+
router.connect();
35+
36+
createRoot(document.querySelector('#root')).render(
37+
<RouterContext value={router}>
38+
<App />
39+
</RouterContext>,
40+
);
41+
```

docs/docs/react/use-navigate.mdx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
export const meta = {
2+
category: 'React',
3+
title: 'useNavigate',
4+
};
5+
6+
# `useNavigate`
7+
8+
React hook for navigate between routes of application.
9+
10+
### Usage
11+
12+
```tsx
13+
import { useNavigate } from '@krutoo/utils/react';
14+
15+
function App() {
16+
const navigate = useNavigate();
17+
18+
const handleAvatarClick = () => {
19+
// navigating to specific route
20+
navigate('/profile');
21+
};
22+
23+
const handleBackClick = () => {
24+
// navigating on history
25+
navigate.go(-1);
26+
};
27+
28+
return (
29+
<main>
30+
<img onClick={handleBackClick} src='/me/avatar.png' />
31+
<button onClick={handleProfileClick}>My profile</button>
32+
{/* ... */}
33+
</main>
34+
);
35+
}
36+
```
37+
38+
### Requirements
39+
40+
You need to wrap your root component to special `RouterContext` to make router specific hooks working:
41+
42+
```tsx
43+
import { createRoot } from 'react-dom';
44+
import { BrowserRouter } from '@krutoo/utils/router';
45+
import { RouterContext } from '@krutoo/utils/react';
46+
import { App } from '#components/app';
47+
48+
const router = new BrowserRouter();
49+
50+
router.connect();
51+
52+
createRoot(document.querySelector('#root')).render(
53+
<RouterContext value={router}>
54+
<App />
55+
</RouterContext>,
56+
);
57+
```
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export const meta = {
2+
category: 'React',
3+
title: 'useRouteParams',
4+
};
5+
6+
# `useRouteParams`
7+
8+
React hook to exec _pathname pattern_ and read params from current location.
9+
10+
### Usage
11+
12+
```tsx
13+
import { useRouteParams } from '@krutoo/utils/react';
14+
15+
function App() {
16+
const { userId } = useRouteParams('/users/:userId');
17+
18+
return <>{/* ... */}</>;
19+
}
20+
```
21+
22+
### Requirements
23+
24+
You need to wrap your root component to special `RouterContext` to make router specific hooks working:
25+
26+
```tsx
27+
import { createRoot } from 'react-dom';
28+
import { BrowserRouter } from '@krutoo/utils/router';
29+
import { RouterContext } from '@krutoo/utils/react';
30+
import { App } from '#components/app';
31+
32+
const router = new BrowserRouter();
33+
34+
router.connect();
35+
36+
createRoot(document.querySelector('#root')).render(
37+
<RouterContext value={router}>
38+
<App />
39+
</RouterContext>,
40+
);
41+
```
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { Callout } from '#components/callout/callout.tsx';
2+
3+
export const meta = {
4+
category: 'Router',
5+
title: 'BrowserRouter',
6+
};
7+
8+
# Router
9+
10+
Package provides `BrowserRouter` - basic implementation for controlling _client routing_ in browser.
11+
12+
<Callout>
13+
<Callout.Heading>SSR ready</Callout.Heading>
14+
<Callout.Main>
15+
Can be used in Node.js for Server Side Rendering or Static Site Generation, see next articles.
16+
</Callout.Main>
17+
</Callout>
18+
19+
### Basic usage
20+
21+
```tsx
22+
import { BrowserRouter } from '@krutoo/utils/router';
23+
24+
const router = new BrowserRouter();
25+
26+
// connect router to Web APIs
27+
router.connect();
28+
29+
// now we can use it to get location info...
30+
console.log(router.getLocation().pathname);
31+
32+
// ...and for redirects
33+
router.navigate('/profile/settings');
34+
35+
// external links also supported
36+
router.navigate('https://google.com');
37+
38+
// you can navigate by history (back for example)
39+
router.go(-1);
40+
```
41+
42+
### React bindings
43+
44+
Package provides some hooks and context for working with router in components.
45+
46+
You need to wrap your root component to special `RouterContext` to make router specific hooks working:
47+
48+
```tsx
49+
import { createRoot } from 'react-dom';
50+
import { BrowserRouter } from '@krutoo/utils/router';
51+
import { RouterContext } from '@krutoo/utils/react';
52+
import { App } from '#components/app';
53+
54+
// we use provided implementation here but you can use your own
55+
const router = new BrowserRouter();
56+
57+
// to make it works you need to call connect
58+
router.connect();
59+
60+
createRoot(document.querySelector('#root')).render(
61+
<RouterContext value={router}>
62+
<App />
63+
</RouterContext>,
64+
);
65+
```
66+
67+
Now you can implement simple routing for example like this:
68+
69+
```tsx
70+
const ROUTES = [
71+
{
72+
path: '/',
73+
render: () => <MainPage />,
74+
},
75+
{
76+
path: '/profile',
77+
render: () => <ProfilePage />,
78+
},
79+
{
80+
path: '/items/:itemId',
81+
render: () => <ItemPage />,
82+
},
83+
];
84+
85+
function App() {
86+
const { pathname } = useLocation();
87+
88+
// find current route
89+
const currentRoute = useMemo(() => {
90+
for (const route of ROUTES) {
91+
const pattern = new URLPattern({ pathname: route.path });
92+
93+
if (pattern.test({ pathname })) {
94+
return route;
95+
}
96+
}
97+
}, [pathname]);
98+
99+
// render current route
100+
<>{currentRoute?.render()}</>;
101+
}
102+
```
103+
104+
And of course you can use other hooks in any of your components:
105+
106+
```tsx
107+
function ItemPage() {
108+
// current location info
109+
const { pathname, hash, search } = useLocation();
110+
111+
// route params
112+
const { groupId, itemId } = useRouteParams('/items/:groupId/:itemId');
113+
114+
// navigate function
115+
const navigate = useNavigate();
116+
117+
return <>{/* ... */}</>;
118+
}
119+
```
120+
121+
### Using for SSR or SSG
122+
123+
`BrowserRouter` can be used in Node.js (or other server environment) to implement Server Side Rendering or Static Site Generation.
124+
125+
You just don't call `connect()` method because under the hood it accesses browser APIs.
126+
127+
Next example shows how you can implement SSR with React:
128+
129+
```tsx
130+
import express from 'express';
131+
import { renderToString } from 'react-dom';
132+
import { BrowserRouter } from '@krutoo/utils/router';
133+
import { RouterContext } from '@krutoo/utils/react';
134+
import { ROUTES } from '#app/routes';
135+
import { App } from '#components/app';
136+
137+
const app = express();
138+
139+
for (const route of ROUTES) {
140+
app.get(ROUTES.path, (req, res) => {
141+
const router = new BrowserRouter({
142+
defaultLocation: { pathname: req.path },
143+
});
144+
145+
const markup = renderToString(
146+
<RouterContext value={router}>
147+
<App />
148+
</RouterContext>,
149+
);
150+
151+
res.send(markup);
152+
});
153+
}
154+
155+
app.listen(8080, () => {
156+
console.log(`Server running at http://localhost:${8080}`);
157+
});
158+
```

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
"./math": "./dist/math/mod.js",
116116
"./misc": "./dist/misc/mod.js",
117117
"./react": "./dist/react/mod.js",
118+
"./router": "./dist/router/mod.js",
118119
"./rspack": "./dist/rspack/mod.js",
119120
"./store": "./dist/store/mod.js",
120121
"./testing": "./dist/testing/mod.js",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { createContext, type Context } from 'react';
2+
import { getStubLocation } from '../../router/utils.ts';
3+
import type { Router } from '../../router/types.ts';
4+
5+
export const RouterContext: Context<Router> = createContext<Router>({
6+
getLocation: getStubLocation,
7+
navigate: () => {},
8+
go: () => {},
9+
subscribe: () => () => {},
10+
connect: () => () => {},
11+
});
12+
13+
RouterContext.displayName = 'RouterContext';

src/react/mod.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,9 @@ export * from './portal.tsx';
5757
// IOC
5858
export { ContainerContext } from './context/container-context.ts';
5959
export { useDependency } from './use-dependency.ts';
60+
61+
// router
62+
export { RouterContext } from './context/router-context.ts';
63+
export { type UseNavigateReturn, useNavigate } from './router/use-navigate.ts';
64+
export { useLocation } from './router/use-location.ts';
65+
export { useRouteParams } from './router/use-route-params.ts';

src/react/router/use-location.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useContext, useState } from 'react';
2+
import { RouterContext } from '../context/router-context.ts';
3+
import type { RouterLocation } from '../../router/types.ts';
4+
import { useIsomorphicLayoutEffect } from '../use-isomorphic-layout-effect.ts';
5+
6+
/**
7+
* Returns current router location state.
8+
* @returns Location.
9+
*/
10+
export function useLocation(): RouterLocation {
11+
const router = useContext(RouterContext);
12+
const [location, setLocation] = useState<RouterLocation>(() => router.getLocation());
13+
14+
useIsomorphicLayoutEffect(() => {
15+
const sync = () => {
16+
setLocation(router.getLocation());
17+
};
18+
19+
sync();
20+
21+
return router.subscribe(sync);
22+
}, [router]);
23+
24+
return location;
25+
}

0 commit comments

Comments
 (0)