• CMSnpm version

    • Overview
    • ContentBuilder
    • InviteUserForm
    • LoginForm
    • RenewPasswordForm
  • UInpm version

    • Overview
    • Accordion
    • AlertBubble
    • AnnouncementBar
    • Avatar
    • AvatarFileInput
    • Badge
    • Button
    • ButtonGroup
    • ButtonList
    • Calendar
    • Checkbox
    • CheckboxButton
    • CheckboxInput
    • CheckboxList
    • Chip
    • ColorRadio
    • ColorRadioGroup
    • Combobox
    • DatePicker
    • DatePickerInput
    • DateRangePicker
    • DateRangePickerInput
    • DatetimePicker
    • DatetimePickerInput
    • Dialog
    • Dropdown
    • Dropzone
    • ErrorMessage
    • FileInput
    • FlashMessages
    • FormComponent
    • Icon
    • IconButton
    • ImageGallery
    • InfoBox
    • Input
    • Label
    • Layout
    • Lightbox
    • ListItem
    • Loader
    • Lozenge
    • Menu
    • Message
    • Modal
    • ✅ ModalDialog
    • ✅ ModalHeader
    • MultiCombobox
    • MultiSelect
    • Pagination
    • Paper
    • Popover
    • Radio
    • RadioGroup
    • RasterImage
    • Select
    • ✅ Tabs
    • TextInput
    • TextLink
    • Textarea
    • TimePicker
    • TimePickerInput
    • Toggle
    • Tooltip
    • Typography
  • Formnpm version

    • Overview
    • AvatarFileInput
    • CheckboxButton
    • CheckboxInput
    • CheckboxList
    • ColorRadioGroup
    • Combobox
    • DatePickerInput
    • DateRangePickerInput
    • DatetimePickerInput
    • Dropzone
    • FileInput
    • Form
    • FormRenderer
    • GpsInput
    • MoneyInput
    • MultiCombobox
    • MultiSelect
    • NumberInput
    • PasswordInput
    • RadioGroup
    • Select
    • TextInput
    • Textarea
    • TimePickerInput
    • Toggle
  • DataGridnpm version

    • Overview
    • DataGrid
    • DataGridCustomExample
    • ExportButton
    • FilterList
    • Filters
    • FiltersButton
    • FulltextInput
    • HiddenColumns
    • HiddenColumnsButton
    • Pagination
    • RowCounts
    • RowsPerPageSelect
    • SelectedRowsToolbar
    • TableV2
    • ToolbarControl
    • ToolbarCustoms
    • ToolbarTabs
  • Wysiwygnpm version

    • Overview
  • Resizernpm version

    • Overview
  • Routernpm version

    • Overview
  • Corenpm version

    • Overview
  • Core-Reactnpm version

    • Overview
  • Stylesnpm version

    • Overview
  • Localizenpm version

    • Overview
  • Analyticsnpm version

    • Overview
  • Datepickernpm version

    • Overview
  • Icons-generatornpm version

    • Overview
  • Smart-addressnpm version

    • Overview
  • E2Enpm version

    • Overview
  • E2E-Playwrightnpm version

    • Overview

@uxf/router

Installation

yarn add @uxf/router

There are two factories:

  • createRouter from @uxf/router is the default. It is server-safe (no React hooks, no client-only imports) and can be used anywhere, including React Server Components. It returns routeToUrl, route, routes, getRouteInfo, createRouteMatcher and createSitemapGenerator.
  • createClientRouter from @uxf/router/client is a superset that adds the React hooks (useQueryParams, useQueryParamsStatic, usePageParams, useRouteInfo). It imports next/navigation and next/router, so it must only be used in client components.

Define the routes once as plain data, then build each router from it:

// routes/routes.ts – plain route definitions, importable anywhere

import { number, object, optional, string } from "superstruct";

export const routes = {
    index: {
        path: "/",
    },
    "admin/index": {
        path: "/admin",
        schema: object({
            param1: optional(number()),
        }),
    },
    "blog/detail": {
        path: "/blog/[id]",
        schema: object({
            id: number(),
        }),
    },
    "localized-route": {
        path: {
            en: "/en/home",
            cs: "/cs/domu",
        },
        schema: object({
            term: optional(string()),
        }),
    },
} as const;

export const routerOptions = {
    locales: ["cs", "en"],
    baseUrl: "https://www.uxf.cz",
} as const;

export type RouteList = typeof routes;
// routes/index.ts – server-safe entry (`@app-routes`), usable in RSC

import { createRouter, ExtractSchema, UxfGetServerSideProps, UxfGetStaticProps } from "@uxf/router";
import { PreviewData as NextPreviewData } from "next/types";
import { RouteList, routerOptions, routes } from "./routes";

export const { route, routeToUrl, getRouteInfo, createRouteMatcher, createSitemapGenerator } = createRouter(
    routes,
    routerOptions,
);

export type GetRouteSchema<K extends keyof RouteList> = ExtractSchema<RouteList[K]>;

export type GetStaticProps<
    Route extends keyof RouteList,
    Props extends { [key: string]: any } = { [key: string]: any },
    PreviewData extends NextPreviewData = NextPreviewData,
> = UxfGetStaticProps<RouteList, Route, Props, PreviewData>;

export type GetServerSideProps<
    Route extends keyof RouteList,
    Props extends { [key: string]: any } = { [key: string]: any },
    PreviewData extends NextPreviewData = NextPreviewData,
> = UxfGetServerSideProps<RouteList, Route, Props, PreviewData>;
// routes/client.ts – client entry (`@app-routes/client`), hooks for client components

"use client";

import { createClientRouter } from "@uxf/router/client";
import { routerOptions, routes } from "./routes";

export const { useQueryParams, useQueryParamsStatic, usePageParams, useRouteInfo } = createClientRouter(
    routes,
    routerOptions,
);

Add configuration to tsconfig.json

{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@app-routes": [
        "routes"
      ],
      "@app-routes/client": [
        "routes/client"
      ]
    }
  }
}

useQueryParams

Hooks live in the client entry (@app-routes/client):

import { useQueryParams } from "@app-routes/client";
import { queryParamToNumber } from "@uxf/router";

// can be used on SSR pages
const [query, { push, replace }] = useQueryParams("route-name");

// must be used on static pages, because router is not ready on first render
// query is null if router is not ready
const [query, { push, replace }] = useQueryParamsStatic("route-name");

Next Link

// pages/index.js

import Link from "next/link";
import { routeToUrl } from "@app-routes";

export default () => (
    <Link href={routeToUrl("blog/detail", { id: 12 })}>
      Hello world
    </Link>
)

RouteMatcher

import { createRouteMatcher } from "@app-routes";

// create active resolver
const routeMatcher = createRouteMatcher("admin/index", { param1: 123 });
// or
const routeMatcher = createRouteMatcher("admin/index");

// how to use in component

function MyComponent() {
    const router = useRouter();
    const isRouteActive = routeMatcher(router);
    
    return <div>{isRouteActive ? "active" : "not active"}</div>;
}

Custom route matchers

function createPathnameRouteMatcher(path: string): RouteMatcher {
    return (router) => {
        return router.pathname.startsWith(path);
    }
}
import { getCurrentRoute } from "@app-routes";

function createCustomRouteMatcher(): RouteMatcher {
    return (router) => {
        const { route, params } = getCurrentRoute(router);
        if (route === "admin/index") {
            // do something
        } else if (route === "admin/form") {
            // do something
        }
    }
}

Merge multiple route matchers

import { mergeRouteMatchers } from "@uxf/router";

const routeMatcher = mergeRouteMatchers([
    createRouteMatcher("admin/index"), 
    createRouteMatcher("admin/form"),
]);

Type-safe route params

import { GetRouteSchema } from "@app-routes";

const blogProps: GetRouteSchema<"blog/detail"> = {
    id: 1,
}

GetStaticProps

import { GetStaticProps } from "@app-routes";
import { queryParamToNumber } from "@uxf/router";

export const getStaticProps: GetStaticProps<"blog/detail"> = (context) => {
    const id = queryParamToNumber(context.params?.id); // context.params is of type { id: number } | undefined
}

GetServerSideProps

import { GetServerSideProps } from "@app-routes";
import { queryParamToNumber } from "@uxf/router";

export const getServerSideProps: GetServerSideProps<"blog/detail"> = (context) => {
    const id = queryParamToNumber(context.params?.id); // context.params is of type { id: number } | undefined
}

Sitemap

Create sitemap items

// sitemap-items.ts in @app-routes

import { createSitemapGenerator, routeToUrl } from "@app-routes";

export const sitemapItems = createSitemapGenerator({baseUrl: 'http://localhost:3000', defaultPriority: 1})
    .add("index", async (route) => ({ loc: routeToUrl(route) }))
    .add("blog/detail", async (route) => [
        { loc: routeToUrl(route, {id: 1}), priority: 2 },
        { loc: routeToUrl(route, {id: 2}), priority: 2 },
    ])
    .skip("admin/index")
    .exhaustive();

sitemap.xml

// pages/sitemap.xml.tsx

import React from "react";
import { NextPage } from "next";
import { sitemapItems } from "@app-routes";

const Page: NextPage = () => null;

Page.getInitialProps = async (ctx) => {
    if (ctx.res) {
        ctx.res.setHeader("Content-Type", "text/xml");
        ctx.res.write(await sitemapItems.toXml());
        ctx.res.end();
    }

    return {};
};

export default Page;

sitemap.json

// pages/sitemap.json.tsx

import React from "react";
import { NextPage } from "next";
import { sitemapItems } from "@app-routes";

const Page: NextPage = () => null;

Page.getInitialProps = async (ctx) => {
    if (ctx.res) {
        ctx.res.setHeader("Content-Type", "text/json");
        ctx.res.write(await sitemapItems.toJson());
        ctx.res.end();
    }

    return {};
};

export default Page;