react-spectrum: `usePress` / `useLink` is not compatible with NextJS's `next/link`

🐛 Bug Report

Because of NextJS’s next/link component’s special behaviour, custom components using the usePress (or useLink) do not work the way I’d expect

🤔 Expected Behavior

When I use a component built with react-aria hooks as a child of next/link, I expect successful client-side navigation to the destination

😯 Current Behavior

Depending on the usage, either I see an error being thrown, or I see successful server-side navigation (a full page reload)

This is the error I see (you’ll be able to see it too in the CodeSandbox example I’ve linked)

Unhandled Runtime Error

TypeError: can't access property "nodeName", e.currentTarget is undefined
linkClicked
node_modules/next/dist/client/link.js (39:27)
onClick
node_modules/next/dist/client/link.js (201:28)
triggerPressEnd
node_modules/@react-aria/interactions/dist/module.js (213:0)
onPointerUp
node_modules/@react-aria/interactions/dist/module.js (427:0)

💁 Possible Solution

React-aria’s PressEvent could expose properties available on the native MouseEvent. Howver, I’m not sure about the feasibility / consequences of this

🔦 Context

We use React-aria in our design system for building a Button component. In our app (which uses NextJS), some buttons only take you to a different page on the app, so I treat them as links (that just happen to look like buttons)

In Next, the recommended way to use links is via their next/link component. That component injects an onClick prop to its first child, and this onClick calls event.preventDefault to be able to do client-side navigation (AKA navigation without a full page refresh)

I expect to be able to use my Button this way:

import Link from 'next/link'

function TakeMeSomewhereNice() {
  return (
    <Link href="/somewhere" passHref>
      <Button />
    </Link>
  )
}

However, this throws an error (you should be able to see this in the CodeSandbox I’ve linked below).

I understand this is quite a niche use-case, and I’m not even sure if react-aria can / should handle this. I can’t imagine where else this problem would pop up, so as far as I know, this issue is limited to next/link.

I have investigated the issue and do have ways to get around this but the solution defeats the purpose of using react-aria in the first place, so it is not ideal. I’m happy to explain in detail exactly what’s going on if you’d like (in Next and react-aria), but I’m not sure if that’s relevant / required right now. For some more context, here is Next’s next/link component: https://github.com/vercel/next.js/blob/5e185fc5da227801d3f12724be3577f4a719aa69/packages/next/client/link.tsx

💻 Code Sample

Here is a minimal repro: https://codesandbox.io/s/next-link-react-aria-incompatibility-l0k0j?file=/pages/index.jsx

The idea is that navigating between the / & /about pages should not reload the page. You’ll see that I have two components on each page. The Button uses the usePress hook, and the Link uses the useLink hook. The Button crashes, while the Link navigates correctly, but the entire page is reloaded.

🌍 Your Environment

Software Version(s)
@react-aria/interactions 3.6.0
@react-aria/link 3.1.4
Browser Irrelevant
Operating System Irrelevant

🧢 Your Company/Team

🕷 Tracking Issue (optional)

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 4
  • Comments: 15 (8 by maintainers)

Most upvoted comments

It should be safe to pass a dummy preventDefault because we already call it in usePress in most cases.

I’ve read this in many many places by now, but it’s really not clear when usePress preventDefault the interaction.

I’m trying to understand usePress.ts code and it seems all events do shouldPreventDefault check before calling preventDefault() but the only one that does not do it is the actual onClick: https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/interactions/src/usePress.ts#L263-L265

I wonder if it should be:

if (isDisabled || shouldPreventDefault(e.currentTarget as HTMLElement)) {
  e.preventDefault();
}

cc: @devongovett

@snowystinger I’m facing the same issue trying to use react-router’s useLinkClickHandler for a Button implemented with useButton (very similar to React Spectrum Button). Your suggestion works to call the click handler in onPress, but the event is not defaultPrevented and therefore a full reload happens. Are you sure preventDefault is already called in most cases? From what I see, preventDefault for click events is called only when the element is disabled

@snowystinger is there any news for this issue? I really like react-aria lib but to be able to use it in our projects we would need a support for nextjs link to work without a full reload. Thank you!

@snowystinger I should have been more clear. I meant button appearance but as an anchor element. Like how you can render a React Spectrum button as an anchor element via elementType. Here is a codesandbox trying to create a LinkButton component inspired by the suggestion from react router docs. preventDefault being a noop function, just prevents the runtime error, but doesn’t prevent the default reload behavior of course. We could even create a useLinkPressHandler hook, similar to useLinkClickHandler, but that would still require ability to call preventDefault on the original event.

There is a solution if you’re using react-aria, you can preventDefault yourself. Here’s a codesandbox based on the one in the description https://codesandbox.io/s/next-link-react-aria-incompatibility-forked-seom13?file=/pages/about.jsx

Bringing it up with the team, I’ll reopen for now for more visibility.