31 December 2022 2 min read

Remix: Treating anchors as client-side links

Sometimes our app renders a <a> anchor element but we want it to work with a client-side navigation instead of a full page refresh. We can emulate what <Link> does in Remix by intercepting these clicks.

Let's dive right into the code:

import { useNavigate } from "@remix-run/react";
import { useEffect } from "react";

export function useClientNavigationLinks() {
  const navigate = useNavigate();

  useEffect(() => {
    const controller = new AbortController();
    document.addEventListener(
      "click",
      (event) => {
        const target = (event.target as Partial<HTMLElement>).closest?.("a");
        if (!target) return;

        const url = new URL(target.href, location.origin);
        if (
          url.origin === window.location.origin &&
          // Ignore clicks with modifiers
          !event.ctrlKey &&
          !event.metaKey &&
          !event.shiftKey &&
          // Ignore right clicks
          event.button === 0 &&
          // Ignore if `target="_blank"`
          [null, undefined, "", "self"].includes(target.target) &&
          !target.hasAttribute("download")
        ) {
          console.log(
            "Treating anchor as <Link> and navigating to:",
            url.pathname
          );
          event.preventDefault();
          navigate(url.pathname + url.search + url.hash);
        }
      },
      { signal: controller.signal }
    );

    return () => controller.abort();
  });
}

This hook will intercept all clicks on anchor elements and if they are internal links, it will call navigate instead of letting the browser do a full page refresh. It handles several edge cases:

Usage

Just call useClientNavigationLinks() in your app root component, or in any route component that renders anchor elements (if you don't want to apply it everywhere).


Props to Jacob Ebey for the idea! I extended the snippet in that Tweet to handle more edge cases (listed above).

Also thanks to Tim (@tpillard) for pointing out that the code can use .closest() instead of an explicit while loop, and also noting another edge case (download attribute).

Tom Sherman

Hey 👋 I'm Tom, a Software Engineer from the UK.

I'm currently a Software Engineer at OVO Energy. I'm super into the web, functional programming, and strong type systems.

You can most easily contact me on Bluesky but I'm also on Twitter, LinkedIn, and GitHub.