Skip to content

TanStack Router

The router entry: RouterUIProvider routes every href the components render through TanStack Router, and useUnsavedChangesBlocker asks before a page with a draft is left. Each example runs its own router in memory.

Language
Appearance

Each link navigates client-side; its href is the URL the router shows.

import { Outlet, RouterProvider, createMemoryHistory, createRootRoute, createRoute, createRouter } from '@tanstack/react-router';
import { useMemo } from 'react';
import type { ReactNode } from 'react';
import { useLocale } from 'react-aria-components';
import { Link } from '@arbol-ai/ui';
import { RouterUIProvider } from '@arbol-ai/ui/router';

const LINK_LABELS: Record<string, string> = {
  '/': 'Overview',
  '/campaigns': 'Campaigns',
  '/contacts': 'Contacts',
  '/settings': 'Settings',
};

interface MemoryRouterExampleProps {
  initialPath: string;
  pages: Record<string, () => ReactNode>;
}

const MemoryRouterExample = ({ initialPath, pages }: MemoryRouterExampleProps) => {
  const router = useMemo(() => createExampleRouter(initialPath, pages), [initialPath, pages]);
  return <RouterProvider router={router} />;
};

const createExampleRouter = (initialPath: string, pages: Record<string, () => ReactNode>) => {
  const paths = Object.keys(pages);
  const rootRoute = createRootRoute({ component: () => <ExampleRoot paths={paths} /> });
  const routes = Object.entries(pages).map(([path, component]) =>
    createRoute({ component, getParentRoute: () => rootRoute, path })
  );
  return createRouter({
    history: createMemoryHistory({ initialEntries: [initialPath] }),
    routeTree: rootRoute.addChildren(routes),
  });
};

const ExampleRoot = ({ paths }: { paths: string[] }) => {
  const { locale } = useLocale();
  return (
    <RouterUIProvider locale={locale}>
      <div className="rounded-card grid gap-4 border p-4">
        <nav aria-label="Example sections" className="flex gap-4 text-sm">
          {paths.map((path) => (
            <Link href={path} key={path}>
              {LINK_LABELS[path]}
            </Link>
          ))}
        </nav>
        <Outlet />
      </div>
    </RouterUIProvider>
  );
};

const OverviewPage = () => <p className="text-sm">Today: 18 appointments and 3 conversations waiting.</p>;

const ContactsPage = () => <p className="text-sm">1,284 contacts, 46 added this week.</p>;

const CampaignsPage = () => <p className="text-sm">2 campaigns running, 1 scheduled for Monday.</p>;

const OVERVIEW_PAGES = { '/': OverviewPage, '/contacts': ContactsPage, '/campaigns': CampaignsPage };

<MemoryRouterExample initialPath="/" pages={OVERVIEW_PAGES} />

Unsaved changes

Change the clinic’s name, then go to Contacts: the page asks first. Save, and it lets you go.

import { Outlet, RouterProvider, createMemoryHistory, createRootRoute, createRoute, createRouter } from '@tanstack/react-router';
import { useMemo, useState } from 'react';
import type { ReactNode } from 'react';
import { useLocale } from 'react-aria-components';
import { Button, Link, TextField, UnsavedChangesDialog } from '@arbol-ai/ui';
import { RouterUIProvider, useUnsavedChangesBlocker } from '@arbol-ai/ui/router';

const LINK_LABELS: Record<string, string> = {
  '/': 'Overview',
  '/campaigns': 'Campaigns',
  '/contacts': 'Contacts',
  '/settings': 'Settings',
};

interface MemoryRouterExampleProps {
  initialPath: string;
  pages: Record<string, () => ReactNode>;
}

const MemoryRouterExample = ({ initialPath, pages }: MemoryRouterExampleProps) => {
  const router = useMemo(() => createExampleRouter(initialPath, pages), [initialPath, pages]);
  return <RouterProvider router={router} />;
};

const createExampleRouter = (initialPath: string, pages: Record<string, () => ReactNode>) => {
  const paths = Object.keys(pages);
  const rootRoute = createRootRoute({ component: () => <ExampleRoot paths={paths} /> });
  const routes = Object.entries(pages).map(([path, component]) =>
    createRoute({ component, getParentRoute: () => rootRoute, path })
  );
  return createRouter({
    history: createMemoryHistory({ initialEntries: [initialPath] }),
    routeTree: rootRoute.addChildren(routes),
  });
};

const ExampleRoot = ({ paths }: { paths: string[] }) => {
  const { locale } = useLocale();
  return (
    <RouterUIProvider locale={locale}>
      <div className="rounded-card grid gap-4 border p-4">
        <nav aria-label="Example sections" className="flex gap-4 text-sm">
          {paths.map((path) => (
            <Link href={path} key={path}>
              {LINK_LABELS[path]}
            </Link>
          ))}
        </nav>
        <Outlet />
      </div>
    </RouterUIProvider>
  );
};

const SettingsPage = () => {
  const [saved, setSaved] = useState('Clínica Norte');
  const [name, setName] = useState(saved);
  const blocker = useUnsavedChangesBlocker(name !== saved);
  return (
    <form
      className="grid max-w-sm gap-3"
      onSubmit={(event) => {
        event.preventDefault();
        setSaved(name);
      }}
    >
      <TextField label="Clinic name" onChange={setName} value={name} />
      <Button className="justify-self-start" type="submit">
        Save
      </Button>
      <UnsavedChangesDialog {...blocker} />
    </form>
  );
};

const ContactsPage = () => <p className="text-sm">1,284 contacts, 46 added this week.</p>;

const SETTINGS_PAGES = { '/settings': SettingsPage, '/contacts': ContactsPage };

<MemoryRouterExample initialPath="/settings" pages={SETTINGS_PAGES} />

Source

The examples above are src/router/router.examples.tsx, which the library’s tests render too.

Type to search…

↑↓ navigate↵ selectEsc close