Skip to content

Instantly share code, notes, and snippets.

@ngbrown
Last active August 6, 2024 07:09
Show Gist options
  • Save ngbrown/c35bbd1e66b615c0d95ff6385d4e5e1f to your computer and use it in GitHub Desktop.
Save ngbrown/c35bbd1e66b615c0d95ff6385d4e5e1f to your computer and use it in GitHub Desktop.
Nav for Remix (and React Router) - Use to drive which component has the active link in a menu
import React from "react";
import type { LinkProps } from "@remix-run/react";
import { stripBasename } from "@remix-run/router";
import {
UNSAFE_DataRouterStateContext as DataRouterStateContext,
UNSAFE_NavigationContext as NavigationContext,
useLocation,
useResolvedPath,
} from "react-router";
import { unstable_useViewTransitionState as useViewTransitionState } from "react-router-dom";
/*eslint prefer-const: "off"*/
export type NavLinkRenderProps = {
isActive: boolean;
isPending: boolean;
isTransitioning: boolean;
};
export interface NavProps
extends Pick<LinkProps, "to" | "unstable_viewTransition" | "relative"> {
children?: (props: NavLinkRenderProps) => React.ReactNode;
caseSensitive?: boolean;
end?: boolean;
}
/**
* A component wrapper that knows if it's "active" or not. Adapted from NavLink.
*/
export function Nav({
caseSensitive = false,
end = false,
to,
unstable_viewTransition,
children,
...rest
}: NavProps) {
let path = useResolvedPath(to, { relative: rest.relative });
let location = useLocation();
let routerState = React.useContext(DataRouterStateContext);
let { navigator, basename } = React.useContext(NavigationContext);
let isTransitioning =
routerState != null &&
// Conditional usage is OK here because the usage of a data router is static
// eslint-disable-next-line react-hooks/rules-of-hooks
useViewTransitionState(path) &&
unstable_viewTransition === true;
let toPathname = navigator.encodeLocation
? navigator.encodeLocation(path).pathname
: path.pathname;
let locationPathname = location.pathname;
let nextLocationPathname =
routerState && routerState.navigation && routerState.navigation.location
? routerState.navigation.location.pathname
: null;
if (!caseSensitive) {
locationPathname = locationPathname.toLowerCase();
nextLocationPathname = nextLocationPathname
? nextLocationPathname.toLowerCase()
: null;
toPathname = toPathname.toLowerCase();
}
if (nextLocationPathname && basename) {
nextLocationPathname =
stripBasename(nextLocationPathname, basename) || nextLocationPathname;
}
// If the `to` has a trailing slash, look at that exact spot. Otherwise,
// we're looking for a slash _after_ what's in `to`. For example:
//
// <NavLink to="/users"> and <NavLink to="/users/">
// both want to look for a / at index 6 to match URL `/users/matt`
const endSlashPosition =
toPathname !== "/" && toPathname.endsWith("/")
? toPathname.length - 1
: toPathname.length;
let isActive =
locationPathname === toPathname ||
(!end &&
locationPathname.startsWith(toPathname) &&
locationPathname.charAt(endSlashPosition) === "/");
let isPending =
nextLocationPathname != null &&
(nextLocationPathname === toPathname ||
(!end &&
nextLocationPathname.startsWith(toPathname) &&
nextLocationPathname.charAt(toPathname.length) === "/"));
let renderProps = {
isActive,
isPending,
isTransitioning,
};
return typeof children === "function" ? children(renderProps) : children;
}
@ngbrown
Copy link
Author

ngbrown commented Aug 5, 2024

Since I was targeting Remix, I used those imports where relevant, but there should be equivalent export LinkProps from React Router. Most of the imports are already from react router.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment