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"
]
}
}
}
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");
// 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>
)
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>;
}
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
}
}
}
import { mergeRouteMatchers } from "@uxf/router";
const routeMatcher = mergeRouteMatchers([
createRouteMatcher("admin/index"),
createRouteMatcher("admin/form"),
]);
import { GetRouteSchema } from "@app-routes";
const blogProps: GetRouteSchema<"blog/detail"> = {
id: 1,
}
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
}
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
}
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();
// 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;
// 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;