For example there are the following routes:
<Routes>
<Route path="admin">
<Route path="users">
<Route index element={<UserList />} />
<Route path="create" element={<UserDetails />} />
<Route path=":id" element={<UserDetails readOnly />} />
<Route path=":id/edit" element={<UserDetails />} />
</Route>
<Route path="roles">
<Route index element={<RoleList />} />
<Route path="create" element={<RoleDetails />} />
<Route path=":id" element={<RoleDetails readOnly />} />
<Route path=":id/edit" element={<RoleDetails />} />
</Route>
</Route>
<Route path="about" />
</Routes>
If the current route is /admin/users/123
then /admin
and /admin/users
links are active. It works fine. But I need to determine whether a link is active manually:
import { ExpandMore } from '@mui/icons-material';
import { ButtonBase, IconButton } from '@mui/material';
import cx from 'classnames';
import { NavLink, To, useMatch } from 'react-router-dom';
interface NavButtonProps {
name: string;
to: To;
}
export function NavButton(props: NavButtonProps) {
const match = useMatch(props.to as string); // What pattern to use here?
const active = Boolean(match);
return (
<ButtonBase component="div" className={cx({ active })}>
<NavLink to={props.to}>
{props.name}
</NavLink>
<IconButton onClick={openMenuWithLinksToNestedRoutes}>
<ExpandMore />
</IconButton>
</ButtonBase>
);
}
How to do it? What pattern should I use? For sure I can see a source code of NavLink
component:
export const NavLink = React.forwardRef<HTMLAnchorElement, NavLinkProps>(
function NavLinkWithRef(
{
"aria-current": ariaCurrentProp = "page",
caseSensitive = false,
className: classNameProp = "",
end = false,
style: styleProp,
to,
children,
...rest
},
ref
) {
let path = useResolvedPath(to, { relative: rest.relative });
let location = useLocation();
let routerState = React.useContext(DataRouterStateContext);
let { navigator } = React.useContext(NavigationContext);
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();
}
let isActive =
locationPathname === toPathname ||
(!end &&
locationPathname.startsWith(toPathname) &&
locationPathname.charAt(toPathname.length) === "/");
But it's really complicated. I think there should be an easier way.
You could let the NavLink
continue doing the work of knowing when it is active and "leak" out to the outer component scope when it is. NavLink
has available to it a children
render function prop that is passed an isActive
prop. The following using a React ref may work for your needs.
export function NavButton(props: NavButtonProps) {
const isActiveRef = React.useRef(false);
return (
<ButtonBase
component="div"
className={cx({ active: isActiveRef.current })}
>
<NavLink to={props.to}>
{({ isActive }) => {
isActiveRef.current = isActive;
return props.name;
}}
</NavLink>
<IconButton onClick={openMenuWithLinksToNestedRoutes}>
<ExpandMore />
</IconButton>
</ButtonBase>
);
}
An alternative is to use a local state in NavButton
and create a "child" component that can pass out the isActive
status via a useEffect
hook.
const Label = ({ setActive, isActive, name }) => {
useEffect(() => {
setActive(isActive);
}, [isActive, setActive]);
return name;
};
export function NavButton(props: NavButtonProps) {
const [active, setActive] = React.useState(false);
return (
<ButtonBase component="div" className={cx({ active })}>
<NavLink to={props.to}>
{({ isActive }) => (
<Label
name={props.name}
isActive={isActive}
setActive={setActive}
/>
)}
</NavLink>
<IconButton onClick={openMenuWithLinksToNestedRoutes}>
<ExpandMore />
</IconButton>
</ButtonBase>
);
}