Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/docs/misc/wait.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,23 @@ wait().then(() => {
console.log('Timeout passed');
});
```

### Aborting

You can pass _abort signal_ from [AbortController](https://developer.mozilla.org/ru/docs/Web/API/AbortController) to reject promise:

```js
import { wait } from '@krutoo/utils';

const controller = new AbortController();

wait(1000, { signal: controller.signal })
.then(() => {
// ...
})
.catch(reason => {
console.log(reason); // "Fake reason"
});

controller.abort('Fake reason');
```
41 changes: 41 additions & 0 deletions docs/docs/react/use-location.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export const meta = {
category: 'React',
title: 'useLocation',
};

# `useLocation`

React hook of state of current location provided by router.

### Usage

```tsx
import { useLocation } from '@krutoo/utils/react';

function ProfilePage() {
const { pathname, hash, search } = useLocation();

return <>{/* ... */}</>;
}
```

### Requirements

You need to wrap your root component to special `RouterContext` to make router specific hooks working:

```tsx
import { createRoot } from 'react-dom';
import { BrowserRouter } from '@krutoo/utils/router';
import { RouterContext } from '@krutoo/utils/react';
import { App } from '#components/app';

const router = new BrowserRouter();

router.connect();

createRoot(document.querySelector('#root')).render(
<RouterContext value={router}>
<App />
</RouterContext>,
);
```
57 changes: 57 additions & 0 deletions docs/docs/react/use-navigate.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
export const meta = {
category: 'React',
title: 'useNavigate',
};

# `useNavigate`

React hook for navigate between routes of application.

### Usage

```tsx
import { useNavigate } from '@krutoo/utils/react';

function App() {
const navigate = useNavigate();

const handleAvatarClick = () => {
// navigating to specific route
navigate('/profile');
};

const handleBackClick = () => {
// navigating on history
navigate.go(-1);
};

return (
<main>
<img onClick={handleBackClick} src='/me/avatar.png' />
<button onClick={handleProfileClick}>My profile</button>
{/* ... */}
</main>
);
}
```

### Requirements

You need to wrap your root component to special `RouterContext` to make router specific hooks working:

```tsx
import { createRoot } from 'react-dom';
import { BrowserRouter } from '@krutoo/utils/router';
import { RouterContext } from '@krutoo/utils/react';
import { App } from '#components/app';

const router = new BrowserRouter();

router.connect();

createRoot(document.querySelector('#root')).render(
<RouterContext value={router}>
<App />
</RouterContext>,
);
```
41 changes: 41 additions & 0 deletions docs/docs/react/use-route-params.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export const meta = {
category: 'React',
title: 'useRouteParams',
};

# `useRouteParams`

React hook to exec _pathname pattern_ and read params from current location.

### Usage

```tsx
import { useRouteParams } from '@krutoo/utils/react';

function App() {
const { userId } = useRouteParams('/users/:userId');

return <>{/* ... */}</>;
}
```

### Requirements

You need to wrap your root component to special `RouterContext` to make router specific hooks working:

```tsx
import { createRoot } from 'react-dom';
import { BrowserRouter } from '@krutoo/utils/router';
import { RouterContext } from '@krutoo/utils/react';
import { App } from '#components/app';

const router = new BrowserRouter();

router.connect();

createRoot(document.querySelector('#root')).render(
<RouterContext value={router}>
<App />
</RouterContext>,
);
```
158 changes: 158 additions & 0 deletions docs/docs/router/browser-router.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { Callout } from '#components/callout/callout.tsx';

export const meta = {
category: 'Router',
title: 'BrowserRouter',
};

# Router

Package provides `BrowserRouter` - basic implementation for controlling _client routing_ in browser.

<Callout>
<Callout.Heading>SSR ready</Callout.Heading>
<Callout.Main>
Can be used in Node.js for Server Side Rendering or Static Site Generation, see next articles.
</Callout.Main>
</Callout>

### Basic usage

```tsx
import { BrowserRouter } from '@krutoo/utils/router';

const router = new BrowserRouter();

// connect router to Web APIs
router.connect();

// now we can use it to get location info...
console.log(router.getLocation().pathname);

// ...and for redirects
router.navigate('/profile/settings');

// external links also supported
router.navigate('https://google.com');

// you can navigate by history (back for example)
router.go(-1);
```

### React bindings

Package provides some hooks and context for working with router in components.

You need to wrap your root component to special `RouterContext` to make router specific hooks working:

```tsx
import { createRoot } from 'react-dom';
import { BrowserRouter } from '@krutoo/utils/router';
import { RouterContext } from '@krutoo/utils/react';
import { App } from '#components/app';

// we use provided implementation here but you can use your own
const router = new BrowserRouter();

// to make it works you need to call connect
router.connect();

createRoot(document.querySelector('#root')).render(
<RouterContext value={router}>
<App />
</RouterContext>,
);
```

Now you can implement simple routing for example like this:

```tsx
const ROUTES = [
{
path: '/',
render: () => <MainPage />,
},
{
path: '/profile',
render: () => <ProfilePage />,
},
{
path: '/items/:itemId',
render: () => <ItemPage />,
},
];

function App() {
const { pathname } = useLocation();

// find current route
const currentRoute = useMemo(() => {
for (const route of ROUTES) {
const pattern = new URLPattern({ pathname: route.path });

if (pattern.test({ pathname })) {
return route;
}
}
}, [pathname]);

// render current route
<>{currentRoute?.render()}</>;
}
```

And of course you can use other hooks in any of your components:

```tsx
function ItemPage() {
// current location info
const { pathname, hash, search } = useLocation();

// route params
const { groupId, itemId } = useRouteParams('/items/:groupId/:itemId');

// navigate function
const navigate = useNavigate();

return <>{/* ... */}</>;
}
```

### Using for SSR or SSG

`BrowserRouter` can be used in Node.js (or other server environment) to implement Server Side Rendering or Static Site Generation.

You just don't call `connect()` method because under the hood it accesses browser APIs.

Next example shows how you can implement SSR with React:

```tsx
import express from 'express';
import { renderToString } from 'react-dom';
import { BrowserRouter } from '@krutoo/utils/router';
import { RouterContext } from '@krutoo/utils/react';
import { ROUTES } from '#app/routes';
import { App } from '#components/app';

const app = express();

for (const route of ROUTES) {
app.get(ROUTES.path, (req, res) => {
const router = new BrowserRouter({
defaultLocation: { pathname: req.path },
});

const markup = renderToString(
<RouterContext value={router}>
<App />
</RouterContext>,
);

res.send(markup);
});
}

app.listen(8080, () => {
console.log(`Server running at http://localhost:${8080}`);
});
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@
"./math": "./dist/math/mod.js",
"./misc": "./dist/misc/mod.js",
"./react": "./dist/react/mod.js",
"./router": "./dist/router/mod.js",
"./rspack": "./dist/rspack/mod.js",
"./store": "./dist/store/mod.js",
"./testing": "./dist/testing/mod.js",
Expand Down
13 changes: 13 additions & 0 deletions src/react/context/router-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createContext, type Context } from 'react';
import { getStubLocation } from '../../router/utils.ts';
import type { Router } from '../../router/types.ts';

export const RouterContext: Context<Router> = createContext<Router>({
getLocation: getStubLocation,
navigate: () => {},
go: () => {},
subscribe: () => () => {},
connect: () => () => {},
});

RouterContext.displayName = 'RouterContext';
6 changes: 6 additions & 0 deletions src/react/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,9 @@ export * from './portal.tsx';
// IOC
export { ContainerContext } from './context/container-context.ts';
export { useDependency } from './use-dependency.ts';

// router
export { RouterContext } from './context/router-context.ts';
export { type UseNavigateReturn, useNavigate } from './router/use-navigate.ts';
export { useLocation } from './router/use-location.ts';
export { useRouteParams } from './router/use-route-params.ts';
25 changes: 25 additions & 0 deletions src/react/router/use-location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useContext, useState } from 'react';
import { RouterContext } from '../context/router-context.ts';
import type { RouterLocation } from '../../router/types.ts';
import { useIsomorphicLayoutEffect } from '../use-isomorphic-layout-effect.ts';

/**
* Returns current router location state.
* @returns Location.
*/
export function useLocation(): RouterLocation {
const router = useContext(RouterContext);
const [location, setLocation] = useState<RouterLocation>(() => router.getLocation());

useIsomorphicLayoutEffect(() => {
const sync = () => {
setLocation(router.getLocation());
};

sync();

return router.subscribe(sync);
}, [router]);

return location;
}
Loading