react-spectrum icon indicating copy to clipboard operation
react-spectrum copied to clipboard

The instructions for client side routing for Tantsack Router doesn't work.

Open MAST1999 opened this issue 1 year ago โ€ข 4 comments

๐Ÿ™‹ Documentation Request

Currently, if you implement it as the documentation has explained, you get this error: image

To get rid of the error, you can change it to this:

declare module 'react-aria-components' {
  interface RouterConfig {
    href: ToPathOption<RegisteredRouter>;
    routerOptions: Omit<NavigateOptions, keyof ToOptions>;
  }
}

But this makes the href becomes just string, and you get no auto complete.

The way to get autocomplete is doing this:

declare module 'react-aria-components' {
	interface RouterConfig {
		href: ToPathOption<RegisteredRouter, '/', '/'> | ({} & string);
		routerOptions: Omit<NavigateOptions, 'to' | 'from'>;
	}
}

And change the router provider to this:

function RootComponent() {
	const router = useRouter();

	return (
		<RouterProvider
			navigate={(to, options) =>
				router.navigate({
					...options,
					to: to as ToPathOption<RegisteredRouter, '/', '/'>,
				})
			}
			useHref={(to) => {
				return router.buildLocation({ to }).href;
			}}
		>
			<Outlet />
		</RouterProvider>
	)
}

Now you will get auto complete in the links, and you can use any string as well: image

This would also enable using the params and search from tanstack/react-router to be typesafe: image

But right now since there is no way to constrict them based on the given href they show all the options for all the routes.

I think the way to fix this would be to give RouterConfig a generic string parameter and pass it to ToPathOption but I'm not sure.

declare module 'react-aria-components' {
	interface RouterConfig<Path extends string> {
		href: ToPathOption<RegisteredRouter, '/', Path> | ({} & string);
		routerOptions: Omit<NavigateOptions<RegisteredRouter, '/', Path>, 'to' | 'from'>;
	}
}

I'm not too familiar with how the types are set up for React Aria, but what I'm hoping for is this will link the href value to ToPathOption and NavigateOptions and show the correct values for search and params options.

๐Ÿงข Your Company/Team

No response

MAST1999 avatar May 18 '24 13:05 MAST1999

I tried to create a Stackblitz for this but uses an older version of typescript and I can't exactly reproduce it: https://stackblitz.com/edit/tanstack-router-hhpjdc?file=src%2Froutes%2F__root.tsx&preset=node

Had to remove the | ({} & string) since it wasn't working the typescript version Stackblitz is using.

MAST1999 avatar May 18 '24 13:05 MAST1999

Here's an example that is passing the params using the routerOptions: https://stackblitz.com/edit/tanstack-router-hhpjdc?file=src%2Froutes%2F__root.tsx,src%2Froutes%2Fposts.tsx&preset=node

MAST1999 avatar May 18 '24 13:05 MAST1999

However, an "official" way to handle this would be much appreciated, so I'm bumping this .. ๐Ÿ™

CHE1RON avatar May 31 '24 11:05 CHE1RON

I played around with the suggested solution by @MAST1999. I needed to do some changes but it's starting to work. routerOptions seems to be typed according to href now but href auto-completion is broken for some reason instead. Still looking into it.

The bigger issue I'm seeing here is that it requires making everything that accepts href and routerOption generic. Also a lot of types in between. I'm unsure if the maintainers of RAC are willing to accept such a PR such for the sake of supporting TanStack Router. I personally went with custom wrapper components for now.

levrik avatar Jun 21 '24 07:06 levrik

Looks like the docs would need to be updated. This used to work but there have been some updates to TanStack Router's types that broke it. This should work instead:

import {ToOptions, NavigateOptions} from '@tanstack/react-router';

declare module 'react-aria-components' {
  interface RouterConfig {
    href: ToOptions['to'];
    routerOptions: Omit<NavigateOptions, keyof ToOptions>;
  }
}

You could also consider typing href as an object containing properties like params as well by making href take ToOptions. See https://github.com/adobe/react-spectrum/issues/6587#issuecomment-2224235268.

devongovett avatar Jul 12 '24 01:07 devongovett

Update: I got it to work by <Omit>ing the conflicting props from React.AnchorHTMLAttributes<HTMLAnchorElement> instead of from AriaLinkOptions. Even still, the behavior between a vanilla Spectrum Link and the Custom Link are different, namely the focus styles


The examples differ between the Spectrum and TanStack docs, but neither works exactly right:

Spectrum: Extend RouterConfig to use TanStack navigation

import {type NavigateOptions, type ToOptions, useRouter} from '@tanstack/react-router';
import {defaultTheme, Provider} from '@adobe/react-spectrum';

declare module '@adobe/react-spectrum' {
  interface RouterConfig {
    href: ToOptions['to'];
    routerOptions: Omit<NavigateOptions, keyof ToOptions>;
  }
}

function RootRoute() {
  let router = useRouter();
  return (
    <Provider
      theme={defaultTheme}
      router={{
        navigate: (to, options) => router.navigate({ to, ...options }),
        useHref: (to) => router.buildLocation({ to }).href
      }}
    >
      {/* ...*/}
    </Provider>
  );
}

Problem with above approach: We lose out on the TanStack goodies such as preload, activeProps, and activeOptions. We're constrained to the props defined on RAC <Link/>.

TanStack: Use createLink to extend with RAC hooks.

import * as React from 'react'
import { createLink, LinkComponent } from '@tanstack/react-router'
import {
  mergeProps,
  useFocusRing,
  useHover,
  useLink,
  useObjectRef,
} from 'react-aria'
import type { AriaLinkOptions } from 'react-aria'

interface RACLinkProps extends Omit<AriaLinkOptions, 'href'> {
  children?: React.ReactNode
}

const RACLinkComponent = React.forwardRef<HTMLAnchorElement, RACLinkProps>(
  (props, forwardedRef) => {
    const ref = useObjectRef(forwardedRef)

    const { isPressed, linkProps } = useLink(props, ref)
    const { isHovered, hoverProps } = useHover(props)
    const { isFocusVisible, isFocused, focusProps } = useFocusRing(props)

    return (
      <a
        {...mergeProps(linkProps, hoverProps, focusProps, props)}
        ref={ref}
        data-hovered={isHovered || undefined}
        data-pressed={isPressed || undefined}
        data-focus-visible={isFocusVisible || undefined}
        data-focused={isFocused || undefined}
      />
    )
  },
)

const CreatedLinkComponent = createLink(RACLinkComponent)

export const CustomLink: LinkComponent<typeof RACLinkComponent> = (props) => {
  return <CreatedLinkComponent preload={'intent'} {...props} />
}

Problem with above approach: AFAIK, AriaLinkOptions doesn't fully extend React.AnchorHTMLAttributes<HTMLAnchorElement> so things like className disappear from CustomLink.activeProps.

The reason I say doesn't fully extend is because I tried the custom link approach and extended both React.AnchorHTMLAttributes<HTMLAnchorElement> and AriaLinkOptions but there is some overlap that causes conflicts (onFocus, onBlur, ...etc). I tried <Omit>ing the conflicting props, but then the RAC hooks were not satisfied. Either way, the HTMLAnchor Props still weren't available in the CustomLink.activeProps.

So.... any ideas?

devbytyler avatar Feb 13 '25 16:02 devbytyler