Client Side Routing

React Aria components like Link, Menu, and Tabs function as navigational links with `href`, supporting attributes like target and download.

Introduction

React Aria components such as Link, Menu, Tabs, and Table transform elements into clickable links that navigate when clicked. These components utilize the href prop to render as an <a> tag, supporting attributes like target and download.

User interactions with these links vary by component. For instance, one might use arrow keys to navigate tabs or press enter to open a link within a ComboBox. With the href prop, React Aria facilitates seamless navigation for each component.

Typically, links perform the default browser action when clicked. However, many applications employ client-side routers to prevent full page reloads. The RouterProvider configures React Aria components to integrate with your client-side router. Simply set it up at the root, and any React Aria component with an href will automatically utilize your router.

Note that links to external sites will default to the browser's native navigation, and links not targeting "_self", using the download attribute, or modified with keys like Command or Alt, will also follow the browser's native behavior.

Router Provider

The RouterProvider component accepts two properties: navigate and useHref. Assign navigate to a function from your routing framework that handles client-side navigation. useHref is optional and modifies a router-specific href to a standard HTML href, such as by adding a base path. Below are setup examples for various frameworks.

import { RouterProvider } from 'react-aria-components';
import { useNavigate, useHref } from 'your-router';
 
export default function Layout() {
  let navigate = useNavigate();
 
  return (
    <RouterProvider navigate={navigate} useHref={useHref}>
      {/* ... */}
    </RouterProvider>
  );
}

Inertia.js

To integrate with Inertia.js, you must first declare it in your .d.ts file, such as in global.d.ts.

import { type VisitOptions } from '@inertiajs/core'
import { type AxiosInstance } from 'axios'
import { type route as routeFn } from 'ziggy-js'
 
declare global {
    interface Window {
        axios: AxiosInstance
    }
 
    let route: typeof routeFn
}
 
declare module 'react-aria-components' {
    interface RouterConfig {
        routerOptions: VisitOptions
    }
}

Next, execute ziggy:generate to generate the Ziggy routes in your terminal.

php artisan ziggy:generate

Then, proceed to alias 'ziggy-js' in your vite.config.ts file.

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
 
export default defineConfig({
    plugins: [
        laravel({
          input: 'resources/js/app.tsx',
          ssr: 'resources/js/ssr.tsx',
          refresh: true,
        }),
        react(),
    ],
    resolve: {
      alias: {
        'ziggy-js': resolve('vendor/tightenco/ziggy'),
        ui: resolve('resources/js/components/ui/index.ts'), // optional if you want to use simple imports
      }
    }
});

Now, create the provider file in resources/js/components/providers.tsx and add the following code:

import { router } from '@inertiajs/react'
import React from 'react'
import { RouterProvider } from 'react-aria-components'
import { ThemeProvider } from './theme-provider'
 
export function Providers({ children }: { children: React.ReactNode }) {
    return (
        <RouterProvider navigate={(to, options) => router.visit(to, options as any)}>
          <ThemeProvider defaultTheme="system" storageKey="theme">
            {children}
          </ThemeProvider>
        </RouterProvider>
    )
}

If you're not sure what is theme-provider, refer to the Theme Provider section. After that, go to your resources/js/app.tsx you can encapsulate <App/> within Providers as follows:

import '../css/app.css'
import './bootstrap'
 
import { Ziggy } from '@/ziggy'
import { createInertiaApp } from '@inertiajs/react'
import { Providers } from 'components/providers'
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
import { createRoot, hydrateRoot } from 'react-dom/client'
import { useRoute } from 'ziggy-js'
 
const appName = import.meta.env.VITE_APP_NAME || 'Laravel'
 
createInertiaApp({
  title: (title) => (title ? `${title} / ${appName}` : appName),
  resolve: (name) => resolvePageComponent(`./pages/${name}.tsx`, import.meta.glob('./pages/**/*.tsx')),
  setup({ el, App, props }) {
    // @ts-expect-error
    window.route = useRoute(Ziggy as any)
    const appElement = (
      <Providers>
        <App {...props} />
      </Providers>
    )
    if (import.meta.env.SSR) {
      hydrateRoot(el, appElement)
      return
    }
 
    createRoot(el).render(appElement)
  },
  progress: false
})

I don't know if you need this or not, but if you care about ssr.tsx, you can add the following code:

import { Ziggy as ziggy } from '@/ziggy'
import { createInertiaApp } from '@inertiajs/react'
import createServer from '@inertiajs/react/server'
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
import ReactDOMServer from 'react-dom/server'
import { route, type RouteName } from 'ziggy-js'
 
const appName = import.meta.env.VITE_APP_NAME || 'Laravel'
 
createServer(
  (page) =>
    createInertiaApp({
      page,
      render: ReactDOMServer.renderToString,
      title: (title) => (title ? `${title} / ${appName}` : appName),
      resolve: (name) => resolvePageComponent(`./pages/${name}.tsx`, import.meta.glob('./pages/**/*.tsx')),
      setup: ({ App, props }) => {
        // @ts-expect-error
        global.route<RouteName> = (name, params, absolute) =>
          // @ts-expect-error
          route(name, params as any, absolute, {
            ...ziggy,
            // @ts-expect-error
            location: new URL(page.props.ziggy.location)
          })
 
        return <App {...props} />
      }
    })
)

Next.js

The useRouter hook from next/navigation provides a router object for navigation purposes. The RouterProvider should be implemented in a client component at the root of each page or layout that contains React Aria links. Create a new client component in the app folder named provider.tsx for this purpose or combine it with other top-level providers as outlined in the Next.js documentation.

'use client';
 
import { useRouter } from 'next/navigation';
import { RouterProvider as RouterProviderPrimitive } from 'react-aria-components';
 
declare module 'react-aria-components' {
  interface RouterConfig {
    routerOptions: NonNullable<
      Parameters<ReturnType<typeof useRouter>['push']>[1]
    >;
  }
}
 
export function RouteProvider({ children }) {
  let router = useRouter();
 
  return (
    <RouterProviderPrimitive navigate={router.push}>
      {children}
    </RouterProviderPrimitive>
  );
}

Then, in app/layout.tsx or your main layout file, enclose the children components within the ClientProviders component.

import { RouteProvider } from './provider';
export default function RootLayout({children}) {
  return (
    <html>
      <body>
        <RouteProvider>{children}</RouteProvider>
      </body>
    </html>
  );
}

Remix

For Remix, we can use the useNavigate and useHref hooks. First, let's create a a file route-provider.tsx in app/components folder.

For comprehensive details, consult the Remix documentation.

Then in app/root.tsx or your main layout file you can use the RouteProvider component to encompass all pages.

Others

If you are using a different framework or router provider not mentioned above, refer to the React Aria Components Docs for additional information on integrating React Aria components with various routers and frameworks.