2.2.0 • Published 11 months ago
@vavra7/router v2.2.0
Async React router supporting SSR
Asynchronous react router supporting a server side rendering. Built to leverage routs based code splitting and displays loading status when chunk of code fetching from server.
Preconditions
Set up react application
Hooks
useLocation()
- returns object of current router locationuseNavigate()
- returns function allowing navigate to another locationuseLocationLoading()
- In case of code splitting and async loading of route. Returns status (boolean) that app is loading chunk of code needed to go to target route.
Components
Link
- renders a tag to given routeOutlet
- renders view
import { Link, Outlet } from '@vavra7/router';
<Link to={{ name: RouteNameEnum.PostDetail, params: { postId: 'a' } }}>post a</Link>
<Outlet />
<Outlet name="widget">
Example of usage
routes definition
Children always means nested route.
import type { RouteConfig } from '@vavra7/router';
import { RouteNameEnum } from '../../../enums/routeName.enum';
import HomeView from '../../../views/home.view';
import NotFoundView from '../../../views/notFound.view';
import Widget1View from '../../../views/posts/widget1.view';
import RootView from '../../../views/root.view';
export const routesConfigs: RouteConfig<RouteNameEnum>[] = [
{
name: RouteNameEnum.Root,
path: '/:lang(en|cs)?',
view: {
component: RootView
},
children: [
{
name: RouteNameEnum.Home,
path: '/',
view: [
{
component: HomeView
},
{
outletName: 'widget',
component: Widget1View
}
]
},
{
name: RouteNameEnum.Posts,
path: '/posts',
view: [
{
loadComponentFce: () =>
import('../../../views/posts.view' /* webpackChunkName: "postsView" */)
},
{
outletName: 'widget',
component: Widget1View
}
],
children: [
{
name: RouteNameEnum.PostDetail,
path: '/:postId',
view: {
loadComponentFce: () =>
import(
'../../../views/posts/postDetail.view' /* webpackChunkName: "postDetailView" */
)
}
}
]
},
{
name: RouteNameEnum.Profile,
path: '/profile',
view: [
{
loadComponentFce: () =>
import('../../../views/profile.view' /* webpackChunkName: "profileView" */)
},
{
outletName: 'widget',
loadComponentFce: () =>
import('../../../views/posts/widget2.view' /* webpackChunkName: "widget2View" */)
}
],
ssr: false,
beforeEnter: async (fromLocation, params) => {
const auth = true;
if (!auth) return { name: RouteNameEnum.Home };
}
}
]
},
{
name: RouteNameEnum.NotFound,
path: '/(.*)',
ssr: false,
view: { component: NotFoundView }
}
];
How to define path
Following rules from this package: https://www.npmjs.com/package/path-to-regexp
Router set up
import type { LocationState } from '@vavra7/router';
import { RouterClient } from '@vavra7/router';
import type { RouteNameEnum } from '../../enums/routeName.enum';
import { routesConfigs } from './routes';
export class Router {
private client?: RouterClient<RouteNameEnum>;
public getClient(): RouterClient<RouteNameEnum> {
if (this.client) {
return this.client;
} else {
return this.initClient();
}
}
private initClient(): RouterClient<RouteNameEnum> {
this.client = new RouterClient<RouteNameEnum>({
debug: process.env.NODE_ENV === 'development',
routesConfigs,
defaultParamsFce: prevParams => ({ lang: prevParams.lang })
});
this.client.locationStore.subscribe(this.onStateChange);
return this.client;
}
private onStateChange(routerState: LocationState): void {
console.log('route has changed')
}
}
export const router = new Router();
Server side
app.use('*', async (req, res) => {
const routerClient = router.getClient();
const { ssr } = await routerClient.navigate(req.originalUrl);
let stringApp = '';
if (ssr) {
stringApp = renderToString(
<RouterProvider client={routerClient}>
<Root />
</RouterProvider>
);
}
const markup = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="main.js" defer></script>
<title>Document</title>
</head>
<body>
<div id="root-slot">${stringApp}</div>
</body>
</html>
`;
res.setHeader('Content-Type', 'text/html');
res.end(markup);
});
Client side
import { RouterProvider } from '@vavra7/router';
import React from 'react';
import { createRoot, hydrateRoot } from 'react-dom/client';
import { router } from './lib/router';
import Root from './root';
async function main(): Promise<void> {
const routerClient = router.getClient();
const { ssr } = await routerClient.navigate(location.pathname + location.search);
const container = document.getElementById('root-slot');
const app = (
<RouterProvider client={routerClient}>
<Root />
</RouterProvider>
);
if (ssr) {
hydrateRoot(container!, app);
} else {
createRoot(container!).render(app);
}
}
main();