react-router v6: get path pattern for current route

Viewed 69868

Is it possible to get the path pattern for the currently matched route? Example:

<Route
    path=":state/:city*"
    element={
        <Page />
    }
/>
// Page.jsx

function Page() {
    ...
    // usePathPattern doesn't actually exist
    const pathPattern = usePathPattern(); // pathPattern = ":state/:city*"
    ...
}

I know I can use useMatch to check if the current location matches a specific path pattern, but then the component has to know what the path pattern is.

12 Answers

I made a custom hook useCurrentPath with react-router v6 to get the current path of route, and it work for me

If the current pathname is /members/5566 I will get path /members/:id

import { matchRoutes, useLocation } from "react-router-dom"

const routes = [{ path: "/members/:id" }]

const useCurrentPath = () => {
  const location = useLocation()
  const [{ route }] = matchRoutes(routes, location)

  return route.path
}

function MemberPage() {
  const currentPath = useCurrentPath() // `/members/5566` -> `/members/:id`
   
  return <></>
}

Reference

https://reactrouter.com/en/v6.3.0/api#matchroutes

I'm not sure if it resolves your use case fully but in my case I used combination of useLocation and useParams. Here is the code:

import React from 'react';
import { useLocation, useParams } from 'react-router-dom';
import type { Location, Params } from 'react-router-dom';

/**
 * Function converts path like /user/123 to /user/:id
 */
const getRoutePath = (location: Location, params: Params): string => {
  const { pathname } = location;

  if (!Object.keys(params).length) {
    return pathname; // we don't need to replace anything
  }

  let path = pathname;
  Object.entries(params).forEach(([paramName, paramValue]) => {
    if (paramValue) {
      path = path.replace(paramValue, `:${paramName}`);
    }
  });
  return path;
};

export const Foo = (): JSX.Element => {
  const location = useLocation();
  const params = useParams();
  const path = getRoutePath(location, params);

  (...)
};

From within the context of a Route you can get the path via several methods layed out in the docs.

My issue was slightly different in that I needed the path outside the context of the Route and came to the below solution that should also work for you:

import {
    matchPath,
    useLocation
} from "react-router-dom";

const routes = [ ':state/:city', ...otherRoutes ];

function usePathPattern() {

    const { pathname } = useLocation();

    return matchPath( pathname, routes )?.path;
}

this is works for me easily

first of all import uselocation from react-router-dom

import { useLocation } from "react-router-dom"

then

const location = useLocation();
console.log(location.pathname);

This seems to work with what they actually export as of 6.2.1, however it uses a component they export as UNSAFE_

import { UNSAFE_RouteContext } from 'react-router-dom';

const reconstructPath = (matches) =>
  matches
    .map(({ route: { path } }) =>
      path.endsWith('/*') ? path.slice(0, -1) : path ? path + '/' : ''
    )
    .join('');

const findLastNode = (node) =>
  node.outlet ? findLastNode(node.outlet.props.value) : node;

const usePathPattern = () =>
  reconstructPath(
    findLastNode(React.useContext(UNSAFE_RouteContext)).matches
  );

I wrote a custom hook for that purpose, since it doesn't seem to be supported oob right now:

Note: it's not thoroughly tested yet. So use with caution.

import { useLocation, useParams } from 'react-router';

export function useRoutePathPattern() {
  const routeParams = useParams();
  const location = useLocation();

  let routePathPattern = location.pathname;

  Object.keys(routeParams)
    .filter((paramKey) => paramKey !== '*')
    .forEach((paramKey) => {
      const paramValue = routeParams[paramKey];
      const regexMiddle = new RegExp(`\/${paramValue}\/`, 'g');
      const regexEnd = new RegExp(`\/${paramValue}$`, 'g');

      routePathPattern = routePathPattern.replaceAll(
        regexMiddle,
        `/:${paramKey}/`,
      );
      routePathPattern = routePathPattern.replaceAll(regexEnd, `/:${paramKey}`);
    });

  return routePathPattern;
}

To find the matched route in react-router v6 you need to use the matchPath function. You will need to store all your routes in a variable like an array. Then you can loop through all routes and use that function to find the matched route. Carefully as it will match the first truthy value, ideally you need to loop through them in the same order you have rendered them.

This isn't a perfect solution as it may not handle nested routes unless you store those in the same variable.

Here's an example:

routes.ts

const routes = [
  { name: 'Home', path: '/home', element: Home },
  { name: 'Profile', path: '/profile', element: Profile },
  { name: '404', path: '*', element: NotFound },
];

App.tsx

<App>
  <BrowserRouter>
    <Header />
    <Routes>
      {routes.map((route, key) => (
        <Route key={key} path={route.path} element={<route.element />} />
      ))}
    </Routes>
  </BrowserRouter>
</App

useMatchedRoute.tsx

import { matchPath } from 'react-router';

export function useMatchedRoute() {
  const { pathname } = useLocation();
  for (const route of routes) {
    if (matchPath({ path: route.path }, pathname)) {
      return route;
    }
  }
}

Header.tsx

export function Header() {
  const route = useMatchedRoute();

  return <div>{route.name}</div>
}

Thank @RichN for his/her answer. I tried to improve/fix his/her answer and also make it compatible with react-router v6.4:

import { UNSAFE_RouteContext } from 'react-router-dom'
function usePathPattern () {
  let lastRouteContext = useContext(UNSAFE_RouteContext)
  while (lastRouteContext.outlet) lastRouteContext = lastRouteContext.outlet.props.routeContext
  return lastRouteContext.matches
    .map(({ route: { path } }) => path).filter(Boolean)
    .join('/').replaceAll(/\/\*?\//g, '/')
}

Sample output:

/:localeCode/:orgName/iaas/:projectSlug/vms/:vmUid/*

Or:

  return lastRouteContext.matches.reduce((patternPath, { route: { path } }) =>
    patternPath + (path
      ? path.endsWith('*') ? path.slice(0, -1) : path.endsWith('/') ? path : path + '/'
      : ''
    ), '')

Sample output:

/:localeCode/:orgName/iaas/:projectSlug/vms/:vmUid/

But ...

... if you (like me) need to know the pattern that matched until here (where usePathPattern() is used), NOT the full-pattern, then you don't need to traverse into route-context to find the last one (findLastNode in @RichN's answer).

So this would be enough:

const usePathPattern = () => useContext(UNSAFE_RouteContext).matches.reduce((patternPath, { route: { path } }) =>
  patternPath + (path
    ? path.endsWith('*') ? path.slice(0, -1) : path.endsWith('/') ? path : path + '/'
    : ''
  ), '')

Sample output:

 # In a near-to-root component:
/:localeCode/:orgName/
# In a very nested component (for the same URL):
/:localeCode/:orgName/iaas/:projectSlug/vms/:vmUid/

I come from Blazor and c# so it is quite different. I think I will still use my own way...

here it is in typescript and using the latest version of react-router dom (V6.3.0).

Basically, I am declaring all of my private route names in an array and then just validating the user's intended path with the array.

Also notice that I am passing the isLogged param so the router knows where to redirect.

From the login component, I can later know where the user wanted to go and redirect them back there after they are logged in. (featured not implemented in this example)

import {useRoutes, useNavigate, useLocation} from "react-router-dom";

export const RouteHandler = ({isLogged} : any) : any => {
let location = useLocation();
let navigate = useNavigate();

let privateRoutes : string[] = [
    "/dashboard",
    "/products"
    //add more private stuff here
];

if(privateRoutes.includes(location.pathname))
{
    if(!isLogged)
    {
        navigate(`/login?return=${location.pathname}`);
    }
}
else
{
    //will go to not found
}

return useRoutes([
    { 
        path: "/login", 
        element: <h2>Login component</h2>
    },
    { 
        path: "/dashboard", 
        element: <h2>You are now logged in</h2>
    },
    { 
        path: "*", 
        element: <h2>Page not found</h2>
    },
]);
}

Here is how to use it

import {RouteHandler } from './mylocation';

<RouteHandler isLogged={true}/>

I hope that it will be useful for some folks in the future.

You cannot at this time. Here is the work around:

<Route path="/:state" element={<LocationView />} />
<Route path="/:state/:city*" element={<LocationView city />} />

I assume you're trying to render the same component under multiple paths. Instead of checking the path pattern, you can add a boolean prop to the component (in this case city) and check if the prop is true.

import { useMatch, Route, Routes } from "react-router-dom";

<Routes>
   <Route path="/" element={<Element1 match={useMatch("/")} />}  >
      <Route path=":id" element={<Element2 match={useMatch("/:id")} />}  />
   </Route>
</Routes>

It's not a direct answer to the question, but those, who found this question while trying to get the params from the match object, this can now be done with the useParams hook.

import { Route, Routes } from 'react-router-dom';
import { useParams } from 'react-router';

<Routes>
   <Route path="/:id" element={<MyComponent />} />
</Routes>

...

function MyComponent() {
  const { id } = useParams();

  return <div>{id}</div>
}

Hope it's useful.

Related