Open chaance opened 7 months ago
Pasting what I said on Twitter for posterity:
It’s very difficult if not impossible to correctly compose components with built in behaviors like this, as I’m sure you know. For example Remix links have a built in click handler, but when used in another component, like a combobox or even a menu, that might not be the only/right interaction. That’s why we haven’t exposed an
as
prop, for example. Doing so would only work correctly 80% of the time, not 100%. This can have downsides for accessibility and cause hard to spot bugs.We think there are usually better ways to solve individual issues that come up than a blunt instrument like
as
orasChild
. In the case of pre-fetch, using a hook in your own wrapper component would probably be a better approach. We’ll definitely add more features to the API as they come up though!
The ideal solution in my view for this case in particular would be if Remix exported their internal usePrefetchBehavior hook, or something like it. Then you could just do something like this:
function MyMenuItem({prefetch, ...props}) {
let [ref, prefetchProps] = usePrefetch(ref);
return <ReactAriaMenuItem {...props} {...prefetchProps} ref={ref} />;
}
This separation of behavior from elements makes sense especially because pre-fetching has nothing to do with the underlying element that gets rendered. Sometimes we actually have to render a different element than a native <a>
due to ARIA/HTML spec limitations, and separating the behavior out would make those work with prefetch too. Remix/React Router normally does this well, e.g. exporting a useLinkClickHandler that you can use to build custom links, but unfortunately not in this case.
Aside from this one in particular, I think we are planning on adding some additional options to pass to the router's navigate function (e.g. preventScrollReset
) and support base path (#5395) in the short term. Longer term we can see what other use cases come up, but I'll definitely discuss your ideas with the team next week. Would be amazing to come up with a solution without the downsides of as
. Thanks for writing this up, I really appreciate it! 😃
I've been struggling to integrate React Router with React Aria Components and agree with the points raised in this issue. I have encountered most of the examples given and they are all quite common use-cases. Another issue I have come up against is working with different kinds of router e.g. hash router or memory router. With a hash router, for example, the RAC Link
component doesn't include the hash in the URL. If I use the useHref
hook from React Router to resolve the URL then the href is correct but the link no longer functions correctly because the navigate function is called with two hashes.
Ultimately, I think the way client side routing works in React Aria needs a bit of a rethink. Simply providing the navigate
function to the RouterProvider
without any control over how it is called is too simplistic. Adding support for additional props at the library level (e.g. preventScrollReset
), however, will always lag behind the features provided by individual routing libraries that users need to access. I am not sure what the best solution is but feel that it's important to be able to easily access all the features that the underlying routing library offers. If it helps, these are some examples of use cases that I feel need elegant solutions:
preventScrollReset
NavLink
component from React RouterCurrent plan for router props is to have a single object that gets passed through to the underlying router, rather than individual props. That way it isn't specific to one router and you don't need to wait when new props are added by your framework.
<MenuItem href="..." routerOptions={{preventScrollReset: true}} />
There is also a way to configure TypeScript to support autocomplete on these that we'll provide docs for.
Something like NavLink I think should be handled by React Aria instead. For example, depending on the component it's used within, the accessibility properties should be different (ie aria-selected
in Tabs rather than aria-current
). We had discussed adding a feature at some point for React Aria link components to know whether they are selected automatically based on the current URL as well, but haven't gotten around to implementing it yet. For now, you should set the selectedKey
prop yourself based on the current url so that the item gets the right states. There's an example in the docs of that.
The href
prop needs to be set to whatever the native URL would be. The hash issue is sort of related to the basepath one (#5395). Somehow the native URL needs to be backwards resolved to the router one. Still thinking about this.
The prefetching one is definitely more complex because it needs actual behavior from the router link, not just settings for the native URL. Somehow we need to separate the individual behaviors from the rendering so that React Aria can do the event handling, and delegate the routing. Without the router separating this out into a hook, it's a bit of a challenge.
Current plan for router props is to have a single object that gets passed through to the underlying router, rather than individual props. That way it isn't specific to one router and you don't need to wait when new props are added by your framework.
This sounds like a good solution and would help a lot.
The href prop needs to be set to whatever the native URL would be. The hash issue is sort of related to the basepath one (https://github.com/adobe/react-spectrum/pull/5395). Somehow the native URL needs to be backwards resolved to the router one. Still thinking about this.
Yes, I think resolving back to the router URL is the important bit.
How would you suggest using React Aria links so that you can provide relative URLs in the same way that you can with the routing library. For React Router, would it be to wrap the components that have an href
prop with custom components that internally use the useHref
hook to determine the URL? My worry is that you still end up having to recreate a lot of the implementation that exists in the router components.
It seems like adding more documentation for how to integrate deeper router functionality with React Aria would be really useful. This may also highlight what can already be achieved in a straightforward way and what needs changes at the library level. Thanks for thinking about this.
Just a thought about resolving the URLs. What if the RouterProvider
had a useHref
prop that takes a hook provided by the router? This hook would resolve the href
prop that the user provides to the native URL in all React Aria components. The user provided href
would be passed through as is to the navigate
function, however, without being transformed. With React Router it would look like this:
import { RouterProvider } from 'react-aria-components';
import { useNavigate, useHref } from 'react-router-dom';
function App() {
const navigate = useNavigate()
return (
<RouterProvider navigate={navigate} useHref={useHref}>
{/* ... */}
</RouterProvider>
);
}
It's a bit unusual to pass a hook as a prop like this but it would resolve the issues with relative paths, base paths and hash routers.
I like that idea! Do you know if next.js has a similar API to resolve URLs? I couldn't find one in their docs.
I like that idea! Do you know if next.js has a similar API to resolve URLs? I couldn't find one in their docs.
I don't think they do. It's worth considering TanStack Router too as the usage will likely grow now that it's reached 1.0 (https://tanstack.com/router/v1). That has the added challenge of providing the same level of type inference via the React Aria link components. It's hard to think of a single approach that will integrate fully with different routing libraries.
FYI I implemented the useHref
idea as well as routerOptions
in https://github.com/adobe/react-spectrum/pull/5864. This should solve most of the issues with links here. The preload case is a remaining problem, until Remix offers hooks or some other programmatic way to trigger preloads.
Provide a general summary of the feature here
We should have a simpler way to render components other than what is returned from the React Aria component by default. This would be either some sort of polymorphic component rendering or a higher level hook.
In the
MenuItem
component (and possibly some others), the current way to render a link is to pass anhref
prop. This simply renders ana
link instead of adiv
. For a library like React Aria, which is typically used as a primitive to create design-system level components, this is quite limiting. There should be some API for rendering another component without dropping down to the lowest-level APIs with React Aria hooks.🤔 Expected Behavior?
TBD (exact behavior depends on the solution)
😯 Current Behavior
Currently I can't get the functionality of a component like
MenuItem
without either rendering that component or recreating it entirely with the much lower-level React Aria hooks.There are a few challenges with the current implementation:
TypeScript. Particularly when using
forwardRef
, theref
's type is always assumed to beHTMLDivElement
. This is a common theme in pretty much every component lib that embraces the "polymorphic" component pattern, and it's difficult to solve without introducing a number of other tradeoffs.Custom link components. React Aria basically assumes you either want a regular
a
for external links or you simply want to navigate using your app's client-side router, in which case it uses the navigator provided toRouterProvider
. I think this leaves a lot to be desired, as router and/or framework link components often bake in other features we lose if rendered in the context of a RA component.💁 Possible Solution
asChild
in Radix UI. This is probably not a great one for React Aria, since its interaction event props (onPress
and family) heavily abstract and hide the underlying DOM props which a lot of components (including RR links) rely on. Leaving React Aria to decide how to resolve likely conflicts is probably going to be a mess.🔦 Context + Examples
Assume here we're talking about rendering a link in
MenuItem
.When the
Link
component in React Router resolves itsto
prop it can determine whether or not a URL value is internal (for client-side routing) or external (leave it to the browser). To do this well it needs to use internal context for apps with abasename
. Recreating this behavior requires that you import non-public context modules and dupe their validation code, which isn't ideal.Another example in React Router is
NavLink
. This component also relies on internal context to determine the link'scurrent
status and supports functional props so you can switch on it.And in Remix, it's pretty much impossible to support their
Link
'sprefetch
functionality without actually rendering theirLink
.🧢 Your Company/Team
Me personally
🕷 Tracking Issue
Couldn't find one!