Custom Links not getting correct href value in the DOM
Which project does this relate to?
Router
Describe the bug
I'm using I followed the docs for creating @chakra-ui/react and was following the docs for creating a custom link.
The routing all works as expected, however when hovering over the links in the browser, I noticed the URL was that of the path for the route I was on.
I did also quickly test is with the exact set up with Material UI as shown in the docs with the same issue.
Your Example Website or App
https://stackblitz.com/edit/github-ygojrn?file=CustomLink.tsx
Steps to Reproduce the Bug or Issue
- Go to https://stackblitz.com/edit/github-ygojrn?file=CustomLink.tsx
- Visit the home page of the App.
- Open the dev tool and look at the link to the About page
- See the href is
/
Expected behavior
I expect that the URL in the browser would reflect the destination path/route.
Screenshots or Videos
https://github.com/user-attachments/assets/e7957293-017d-4d70-8057-0461fd0ae9ef
Platform
- OS: macOS
- Browser: Chrome
- Version: 129.0.6668.90
Additional context
No response
Confirmed this is not a Chakra-specific issue. This is happening with MUI as well. https://stackblitz.com/edit/github-ygojrn-jjltmm?file=src%2Froutes%2Findex.tsx
I have the same issue with a fairly simple custom link component. Navigation and prefetching on hover intent etc works as normal.
interface NavLinkProps extends Omit<RACLinkProps, 'className' | 'slot' | 'children' | 'style'> {
children?: React.ReactNode
}
export const NavLink = createLink(
React.forwardRef<React.ElementRef<typeof Link>, NavLinkProps>(
({ isDisabled, children, ...rest }, forwardedRef) => {
const ref = useObjectRef(forwardedRef)
const { isHovered, hoverProps } = useHover({ isDisabled, ...rest })
const { isPressed, linkProps } = useLink({ isDisabled, ...rest }, ref)
return (
<Link
{...mergeProps(hoverProps, linkProps, rest)}
ref={ref}
data-hovered={isHovered || undefined}
data-pressed={isPressed || undefined}
className={navItemStyles({ isDisabled, isHovered, isPressed })}
activeOptions={{ includeSearch: false }}
>
{children}
</Link>
)
},
),
)
I’ve altered the reproduction to use the approach I’m using at work and it works fine:
https://stackblitz.com/edit/github-ygojrn-pwpojs?file=CustomLink.tsx,src%2Froutes%2Findex.tsx,src%2Fmain.tsx
I’ve removed forwardRef because it’s a pain but I don’t think that’s the issue. Feel free to re-add it.
I've added the react-aria-components example issue to @TkDodo's reproduction.
https://stackblitz.com/edit/github-ygojrn-wrompg?file=src%2Froutes%2Findex.tsx
Here the CustomRACLink links will get the current page as the href.
I don’t think you’re supposed to render a <Link> from the TanStack Router inside createLink, because that’s what createLink does. Look at how it’s implemented (with types stripped):
export function createLink(Comp) {
return React.forwardRef(function CreatedLink(props, ref) {
return <Link {...props} _asChild={Comp} ref={ref} />
})
}
that means inside of createLink, you’re supposed to render your Link abstraction, like a ReactAriaLink or a ChakraLink, not a TanStackRouter Link. This is what my solution does and it works well with all kinds of link implementations.
That makes sense, but then the docs are confusing to me, or I'm simply just reading it completely wrong.
For anyone coming across this using React Aria Components:
Intent preload is lost when using the RAC Link component, due to it not supporting onMouseEnter and onMouseLeave.
Their components are using onHoverStart and onHoverEnd instead.
https://react-spectrum.adobe.com/blog/building-a-button-part-2.html#the-usehover-hook
Thanks @TkDodo !
yeah seems like the docs need to be updated
I'll take a crack at it asap.
Oh glad I found this, been cracking my head round this for the last hour or so.
I liked the approach as described in the current docs, as you can add some default options to the Link component this way. The new approach looses that functionality.
Maybe it can be done like so:
const CustomLink = createLink((props) => <a {...props} />, {
activeOptions: { exact: false },
activeProps: { ['data-active']: '' }
})
@Pagebakers
You can still do that. I have tried to indicate that in the revised docs by including the Link-specific prop: preload={'intent'} before spreading in other props.
import * as React from 'react'
import { createLink, LinkComponent } from '@tanstack/react-router'
interface BasicLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
// Add any additional props you want to pass to the anchor element
}
const BasicLinkComponent = React.forwardRef<HTMLAnchorElement, BasicLinkProps>(
(props, ref) => {
return (
<a ref={ref} {...props} className={'block px-3 py-2 text-blue-700'} />
)
},
)
const CreatedLinkComponent = createLink(BasicLinkComponent)
export const CustomLink: LinkComponent<typeof BasicLinkComponent> = (props) => {
return (
<CreatedLinkComponent
preload={'intent'}
activeOptions={{ exact: false }}
// other Link-specific props.
{...props} />
)
}
closing this now that the docs have been updated. please open a new issue if there are still open issues
We haven't been able to get this to work with Mantine's Menu.Item component which can be a Link (or anchor) under the hood, if anyone else has gotten this to work (I tried TkDodo's solution but it seems to result in unrenderable HTML, something incorrectly the child of an anchor or something), I would be very grateful if they shared how.