2.2.0 • Published 11 months ago

@vavra7/router v2.2.0

Weekly downloads
-
License
MIT
Repository
-
Last release
11 months ago

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 location
  • useNavigate() - returns function allowing navigate to another location
  • useLocationLoading() - 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 route
  • Outlet - 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();
1.2.0

12 months ago

1.0.2

12 months ago

2.2.0

11 months ago

1.1.0

12 months ago

2.1.0

11 months ago

2.0.0

11 months ago

1.0.1

1 year ago

1.0.0

1 year ago