This misses event.shiftKey. All keyboard modifiers disqualify client-side routing.
As for <Link> in its entirety, where you have to use a special component which becomes a link and adds a click handler to it, I think this is generally the wrong solution: it’s better to instead put a single click handler on the entire document to intercept clicks on links. That way, you can just use normal links everywhere and it just works, rather than having to remember to use a whole separate component every time which may or may not be able to pass through the required properties or attributes (e.g. this one only supports class and href). Simpler, smaller and cheaper.
I think React tends to encourage local solutions (using a Link component) over global ones (inserting a global click handler) when possible. Global solutions are less composable and will cause issues if you have multiple independent teams working on the same page (eg maybe they need different click handlers)
In general I would agree, but as soon as you’re using popstate, you’ve broken that completely: it’s inherently global and will conflict with any other compositions. Because of this, the use of a global click handler will make the application more consistent: either all forms of navigation work, or none; whereas if you use <Link> it gives the impression that you can compose, but the back and forward buttons may be broken.
The History API, as it stands, is global state. That makes it one of the very few places where I say it is better to use a global solution, rather than a precise one.
> In general I would agree, but as soon as you’re using popstate, you’ve broken that completely: it’s inherently global and will conflict with any other compositions
It can be problematic to access globals from functional programming contexts, but I don't really see how History would cause conflicts here. Do you have any examples?
If you are really worried about this then the proper way to solve it imo is to pass `history` in as a prop or Context instead of calling it from the global `window` directly. This follows functional paradigms and is more in line with React principles. This has some other advantages as well. For example, if you are using test frameworks like Jest, you can test the link component by mocking the History object being passed in.
The URL is global state. Only one thing can use it to control what’s displayed. It is an error to manipulate `location` or `history` at the widget or library level: they must only be used at the application level.
Passing history in as a property doesn’t solve anything in the application: it’s still global state, and if multiple things try to use it (even if one is trying to use the hash and the other the path), they’ll interfere with each other and you’ll have a bad time.
Passing it as a property may help with mocking-based test frameworks (though I find mocking to normally be a bad technique—you get better results from unit tests and integration tests with real systems), though you may also be able to control the global variables to accomplish the same effect; yet as far as the application is concerned, it’s still global state.
So then: since anything working with the URL is unavoidably working with global state, I see no virtue in avoiding a global event handler.
Global state is never actually necessary. Functional programming doesn't require global state to be turing complete. So I don't see what you mean by it being unavoidable. Passing it in makes it not global state anymore. As I explained in the mocking example. You can run multiple tests at once while initializing a different mock History object to be passed in for every test.
In my experience, generally when people try to use global handlers, it ends up getting bloated as more and more teams add their quirks and features into those handlers. There are numerous ways to mitigate this but I've found that the easiest way to prevent these issues is to simply move the logic into components. And React context gets rid of the need for prop drilling and removes most of the boilerplate involved with passing data to deeply nested components.
The middle mouse button doesn’t trigger a click event, but rather auxclick (like right click also doesn’t, but rather contextmenu and then auxclick if that’s preventDefaulted).
But there are definitely situations where it’s judicious to check event.button, and I missed altKey, too. Here’s the full function Fastmail uses:
Ah indeed, I probably confused it with other mouse events that do not have a button-specific event like `auxclick` and `contextmenu`. Mouse down/up only use the button to differentiate the buttons, which can also explain why that function includes such check (even if unnecessary on the `click` event)
On a side note, I can't believe we still don't have a `visit` or `activate` event that works regardless of hardware, without having to exclude modifier keys.
I don’t think the React root is really any better than the document—you should either go precise, or go global, since popstate is global. If there are some links you don’t want intercepted, they’re as likely to be within the React root as without it. See also my response to woojoo666.
addEventListener("click", function (event) {
const link = event.target.closest("a");
if (
!event.button &&
!event.altKey &&
!event.ctrlKey &&
!event.metaKey &&
!event.shiftKey &&
link &&
link.href.startsWith(location.origin + "/") &&
link.target !== "_blank"
) {
event.preventDefault();
navigate(link.href);
}
}
There are some points of nuance here where you may want to vary (e.g. the determination of eligible hrefs, and handling of href targets), but this is the bones of it. You can also choose to add more functionality to it. Fastmail’s webmail, for example, uses this basic design, but also handles mailto: links specially (taking you to compose a new email), whitelists path patterns (since the domain is used for more than just the webmail app), and one or two other things.
It gets called on all clicks (line 1, it’s a window-level click handler), then finds the link the click target is inside (line 2), and does nothing if it wasn’t inside a link (line 9).
(You might think that it should use .closest("a[href]") instead of .closest("a"), since a:not([href]) is not a link, but in that case, link.href === "", and so it fails the line 10 is-it-eligible-for-client-side-routing test. Note also that except for the “no href attribute” case, HTMLAnchorElement#href gives a full resolved URL, so <a href=""> will produce the document base URL.)
Fundamentally, there’s no such thing as an event handler that triggers only on links—you instead have to use a global event handler that starts by checking whether it’s being triggered on a link.
I do think, though, that woojoo666’s comment and my response are worth bearing in mind. In this specific situation, I think <Link> is not warranted and mildly better avoided; but in a similar situation where global behaviour wasn’t already forced, I would say to keep the separate component, rather than breaking behavioural encapsulation.
This misses event.shiftKey. All keyboard modifiers disqualify client-side routing.
As for <Link> in its entirety, where you have to use a special component which becomes a link and adds a click handler to it, I think this is generally the wrong solution: it’s better to instead put a single click handler on the entire document to intercept clicks on links. That way, you can just use normal links everywhere and it just works, rather than having to remember to use a whole separate component every time which may or may not be able to pass through the required properties or attributes (e.g. this one only supports class and href). Simpler, smaller and cheaper.