Skip to content

Instantly share code, notes, and snippets.

@Talor-A
Last active May 8, 2024 20:02
Show Gist options
  • Save Talor-A/edb3797cdaa0ccd18efda997c003fef3 to your computer and use it in GitHub Desktop.
Save Talor-A/edb3797cdaa0ccd18efda997c003fef3 to your computer and use it in GitHub Desktop.
connect react-aria-components with next router
export function MyMenu() {
return (
<Menu>
<MenuItem
href={{
pathname: '/about',
query: { name: 'test' },
}}>
About Page
</MenuItem>
</Menu>
)
}
import {
MenuItem as AriaMenuItem,
MenuItemProps as AriaMenuItemProps
} from 'react-aria-components';
interface MenuItemProps<T>
extends ReactAriaNextLinkProps,
Omit<AriaMenuItemProps<T>, 'children' | 'href'> {
}
function MenuItem<T extends object>(
{
children,
href,
as,
shallow,
scroll,
...props
}: MenuItemProps<T>,
) {
return (
<AriaMenuItem<T>
{...props}
/**
* using this function, we can externally accept `href={string|UrlObject}` and
* `as={string|UrlObject}`. then, they get passed to this function and mapped over to
* `href={string}` and `routerOptions={RouterOptions}`. this keeps the same external
* API as `next/link`.
*/
{...nextRouterPropsToReactAriaProps({ href, as, shallow, scroll })}
>
{children}
</AriaMenuItem>
);
}
import type { Url } from 'next/dist/shared/lib/router/router';
import type { LinkProps as NextLinkProps } from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import { useCallback } from 'react';
import { RouterProvider, type LinkProps } from 'react-aria-components';
/**
* copied from next/router.d.ts
*/
interface TransitionOptions {
shallow?: boolean;
locale?: string | false;
scroll?: boolean;
unstable_skipClientCache?: boolean;
}
interface NextRouterOptions extends TransitionOptions {
as?: Url;
/** allow passing href as an object. this will override the path passed to the `href` prop of the component. */
href?: Url;
}
declare module 'react-aria-components' {
interface RouterConfig {
/**
* we're using interface merging to set `routerOptions` to accept props that `next/router` wants.
*
* this allows every `routerOptions` prop on a component from `react-aria-components` to accept
* the right next.js props with typesafety.
*/
routerOptions: NextRouterOptions;
}
}
interface NextRouterProviderProps {
children: React.ReactNode;
}
type Navigate = (
path: string,
routerOptions: NextRouterOptions | undefined,
) => void;
export function NextRouterProvider(props: NextRouterProviderProps) {
const { push } = useRouter();
const navigate = useCallback<Navigate>(
/**
* path is the `href` prop that's passed to the component, and is always a string.
*
* however, we'll usually expect `nextRouterPropsToReactAriaProps` to have set `href` for us,
* so this fallback will only come into play if you're not using that helper.
*/
(path, { as, href, ...options } = {}) => {
push(href || path, as, options);
},
[push],
);
return <RouterProvider navigate={navigate}>{props.children}</RouterProvider>;
}
/**
* react-aria exposes a `href:string` prop and a `routerOptions` catchall prop on
* many of its components like `Link`, `MenuItem`, etc.
*
* it's more convenient to pass `href` / `as` / `shallow` as you would on a
* regular `next/link` component, and define `href` as a `Url` object if we want to.
*
* this interface defines the props from `next/link` that we want to proxy over to the
* right place to be used by stuff from `react-aria-components`.
*/
export interface ReactAriaNextLinkProps
extends Partial<Pick<NextLinkProps, 'as' | 'href' | 'shallow' | 'scroll'>> {}
type RequireExplicitlyUndefined<T> = {
[P in keyof Required<T>]: T[P] | undefined;
};
/**
* react-aria exposes a `href:string` prop and a `routerOptions` catchall prop on
* many of its components like `Link`, `MenuItem`, etc.
*
* it's more convenient to pass `href` / `as` / `shallow` as you would on a
* regular `next/link` component, and define `href` as a `Url` object if we want to.
*
* this function maps the props that are more ergonomic to use over to the right
* props that components from `react-aria-components` expect.
*
* when we create a wrapper around a `react-aria-components` component, we should use
* this function inside our wrapper.
*/
export const nextRouterPropsToReactAriaProps = (
props: RequireExplicitlyUndefined<ReactAriaNextLinkProps>,
): Pick<LinkProps, 'href' | 'routerOptions'> => {
const { as: asProp, href, shallow, scroll } = props;
if (!href) {
return {};
}
return {
href: (asProp || href).toString(),
routerOptions: {
as: asProp,
href,
shallow,
scroll,
},
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment