-
Notifications
You must be signed in to change notification settings - Fork 27.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] useLink #7329
Comments
Haven't thought deeply about this yet, but does an additional programmatic API for this make sense? // pages/index.js
import { useRoute } from 'next/router'
function HomePage() {
const { push, replace } = useRoute('/about', () => import('./about'))
return <>
Go to about page
<button onClick={push}>About</button>
<button onClick={replace}>About (replace)</button>
</>
} |
It wouldn't because at that point we can't do prefetching automatically based on the viewport which is what we currently do for Note that |
Makes sense. I was more thinking about the linting part, but that's probably not a big enough win to increase the API surface by so much. |
|
Oh right, now I see. Thanks, that's a very elegant API 👍 |
So would users be able to change the route string but not the page component file name in this new API? Or do the route name and file name need to match? |
Hey @timneutkens, thank you for writing this up. Interesting approach. Few questions though:
// links.ts
export const useAboutLink = () => useLink('/about', () => import('./about'))
export const useArticleLink = () => useLink('/article', () => import('./article')) // pages/index.ts
import { useAboutLink } from '../links'
export default () => {
const AboutLink = useAboutLink()
return (
<nav>
<AboutLink>About</AboutLink>
</nav>
)
} I use similar approach with current Link and so far it works fine. It's actually "one line easier", because I can import the link directly. Thanks to tree-shaking, the bundle should always have just the links included on page and not the full route config.
Thanks for the hard work 🙏 |
Is this the proposal to deal with dynamic routes that you were referring to in your comment on the thread on Dynamic Routing and in the RFC on API routes? I noticed that you didn't list #4989 as a closable issue in your comment above. |
I love the idea of using a hook API for links. One of the most awkward APIs in Next is the way the links work. They are:
I'd suggest that the goal of the link API should be:
For example, in use-next-route I used this API: const { href, onClick, navigate } = useLink('/projects') This lets me decouple the styled links from next functionality. My projects will usually have a set of low-level UI components. Let's say I'm using styled-components: const { href, onClick, navigate } = useLink('/projects')
return (
<StyledLink href={href} onClick={onClick} />
) or if I want a button click to trigger a navigation, the API stays the same: const { href, onClick, navigate } = useLink('/projects')
return (
<button onClick={onClick}>Go there</button>
) or if I want to navigate on a form submission: const { href, onClick, navigate } = useLink('/projects')
function onSubmit() {
// handle the form
navigate()
}
return (
<form onSubmit={navigate}>...</form>
) There's also the problem wanting to pass in parameters. You can do this when you create the link, just like in const projectRoute = useRoute({
pathname: '/project/details',
query: {
id: props.project.id
}
}) or you can pass in params when you call const { navigate } = useLink()
function onSubmit() {
navigate({
pathname: '/project/details',
query: {
id: props.project.id
}
})
} One benefit of doing it this way is you can pull out all of your routes into another file, so you can re-use the logic/parameters, regardless of what type of element is trying to trigger the navigation. In my apps, I have a export function useProjectRoute(projectId: string) {
return useRoute({
pathname: '/project/details',
query: {
id: projectId
}
})
} If we were to return an anchor element it limits the ways the route could be used. Let's say we tried to follow a similar pattern: export function useProjectRoute(projectId: string) {
return useLink('/projects/details', () => import('./about'))
} This returns an I'm not too sure how this might affect the ability to create a dependency graph, but maybe we could have a combination of the two: return { href, onClick, navigate } = useLink('/project/details', () => import('/project/details')) with the option to pass in parameters: return { href, onClick, navigate } = useLink({
pathname: '/project/details'
}, () => import('/project/details')) But it looks a little awkward. I'm not too sure about the mechanics of the |
Hey, this sounds very promising (really love all your RFCs lately!). In all the projects I've worked on we've always rather used Some examples I can think of:
The last one is probably the biggest one. I mentioned in the other RFC that we serve everything under an asset prefix I guess all of this is probably possible with Would this proposal be able to still do prefetching if we wrap In any case I love the |
Interesting 🤔 I actually like how
@anthonyshort I use <Link href="/about" passHref>
<StyledButton>About</StyledButton>
</Link> But again, this solution is beautiful that On the other hand, having one component to do both routing and rendering might look simpler but it's less flexible. It's harder to update Link API (add new prop) as now it might break any existing components which use the same prop. The props namespace might be a problem even when you want to reuse 3rd party component as a Link.
It isn't a magic, just not a pattern which you see very often, yet. Reach UI does amazing things with child components. It certainly makes the public API simpler. You could have an explicit render prop pattern:
It's more explicit, but harder to write and read.
If the linter complains about a valid code, then it's broken and should be disabled or replaced with more appropriate one. |
How would you manage rendering links to unknown routes with this API? For example, in our app we receive a list of menu links from an external CMS. Since our links are dynamic, this proposal wouldn't work for us. We wouldn’t know which file to import and we would have to use string interpolation in the import statements. From what I understand, interpolated import statements cause a lot more code to be shipped to the client than expected (Webpack has to bundle every possible match). Here's an example to illustrate the issue I'm describing: // Topbar.js
function Topbar(props) {
const { links } = this.props
return (
<header>
{...links.map(link => (<TopbarLink {...link}/>))}
<header/>
)
}
// TopbarLink.js
import { useLink } from 'next/link'
function TopbarLink(props) {
const { page, text } = props
const Link = useLink(page, () => import(`./${page}`))
return <>
<Link>{text}</Link>
</>
} |
Hey all! This is my first comment to Next.js in a very very long time :) TL;DR: So freaking excited to be here. Hope I can help Next.js grow! At first look, a few thing feel funny here:
But, also getting some good vibes:
This starts to feel like a lot of responsibility is being handed to the user where the framework should be able to handle the magic instead. Unfortunately though, I can't think of a better way to achieve the benefits in the OP. |
@timneutkens how does this RFC interop w/ dynamic routes? Would love to see some examples in the OP. My guess: const BlogLink = useLink(`/blog/${blogID}`, () => import('./blog')) |
I'm going to close this RFC as it has quite a few downsides as pointed out by Sebastian from the React team. We're working on improving the existing next/link component. |
Hi @timneutkens that's too bad, it was very interesting! Are Sebastian's comment public to read somewhere or was it private or a conversation? Just curious to know more about the limits he identified. |
Private. Main limitation is returning a component from a hook, which causes some issues with memoization. |
Alright, thanks, good to know! |
This issue has been automatically locked due to no recent activity. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you. |
Goals
cloneElement
href
vsas
next/link
but retaining backwards compatibility with it.buildId
in the page file url to improve long-term caching.API
Simple usage:
So what does
useLink
return?An enhanced
<a>
component withhref
,onClick
andref
setAutomatically prefetches based on viewport
import()
allows linters to throw when the file doesn't exist (TS, eslint etc)You no longer have to write
<a>
inside of<Link>
Alternatives
An alternative API that involves code-generation and is harder to type using TypeScript would be:
It would give the same linting / 404 detection benefits as the import API, we'd abstract it away from the user using code generation.
One issue that was raised by @lydiahallie is that creating a navbar would look like this:
The solution for doing something like that might be using returned hook value:
The text was updated successfully, but these errors were encountered: