One RouterProvider at the app's root routes every link in the library through your router.

One provider

Every link in the library is React Aria's Link. Put one RouterProvider at the app's root, and a plain press on any of them calls your router. These still fall through to the browser:

  • A press with a modifier key.
  • A link with target or download.
  • A link to another origin.

With no provider, a link follows its href as the browser would. The server's HTML is a real link either way, so it works with scripts off.

Import RouterProvider from @snapshot-ui/react. It is the library's own copy of React Aria's, so the provider and every link share one router, whatever else the app installs. With the Next.js App Router, wrap the app in a small client component, and augment RouterConfig so routerOptions is typed:

'use client';

import { useRouter } from 'next/navigation';
import type { ReactNode } from 'react';
import { RouterProvider } from '@snapshot-ui/react';

declare module '@snapshot-ui/react' {
  interface RouterConfig {
    routerOptions: NonNullable<Parameters<ReturnType<typeof useRouter>['push']>[1]>;
  }
}

export function ClientRouter({ children }: { children: ReactNode }) {
  const router = useRouter();
  return (
    <RouterProvider
      navigate={(href, options) => {
        router.push(href, options);
      }}
    >
      {children}
    </RouterProvider>
  );
}

Render <ClientRouter> around {children} in app/layout.tsx. routerOptions on a link are passed to the router for that press, such as { scroll: false }.

What goes through it

Every part that draws a link:

Prefetch

React Aria renders a plain <a>, so Next does not prefetch these pages as links come into view. Most links are fine without it. Prefetch pays in always-visible navigation, such as a rail or a tab bar, and there are two ways to get it back.

Prefetch on intent, as the reader moves toward the link:

'use client';

import { useRouter } from 'next/navigation';
import { SidebarItem } from '@snapshot-ui/react';
import { Wallet } from 'lucide-react';

export function TransactionsItem({ isCurrent }: { isCurrent: boolean }) {
  const router = useRouter();
  const prefetch = () => {
    router.prefetch('/transactions');
  };
  return (
    <SidebarItem href="/transactions" icon={<Wallet />} isCurrent={isCurrent} onHoverStart={prefetch} onFocus={prefetch}>
      Transactions
    </SidebarItem>
  );
}

Or render Next's own Link through the render prop, for its viewport prefetch. Every link part has render except a linked StatTile, which also takes no routerOptions. render is a function, so pass it from a client component, which the app already has for usePathname():

'use client';

import NextLink from 'next/link';
import { SidebarItem } from '@snapshot-ui/react';
import { Wallet } from 'lucide-react';

export function TransactionsItem({ isCurrent }: { isCurrent: boolean }) {
  return (
    <SidebarItem
      href="/transactions"
      icon={<Wallet />}
      isCurrent={isCurrent}
      render={(props) => <NextLink {...props} href="/transactions" />}
    >
      Transactions
    </SidebarItem>
  );
}

No linkComponent

The library takes no linkComponent prop:

  • It would be a second spelling of render.
  • Like render, it could not be passed from a server component.
  • It would have to be threaded through every group, item, brand and bar.

One provider covers the common case, and render covers the one link that needs more.

esc

↑ ↓ to move↵ to open