86

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.

caseyjhol
  • 3,090
  • 2
  • 19
  • 23

17 Answers17

56

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

Michael Mior
  • 28,107
  • 9
  • 89
  • 113
Futian Shen
  • 569
  • 3
  • 3
  • 4
    This worked great for me. The custom hook is a personal preference. However, `useLocation` + `matchRoutes` is the answer. – Herman J. Radtke III May 01 '22 at 19:33
  • 1
    This doesn't appear to work with nested routes with relative paths. For example, when `location.pathname` is `"/users/123/profile"` but `routes[0].path` is `"profile"` due to being nested under `"users/:id"`. Do you have a solution for that? – eliw00d Jul 28 '22 at 20:56
  • @eliw00d I had the same issue with nested routes. I solved this by searching the matching routes by the pathname: `matchRoutes(routes, location).find(m => m.pathname === location.pathname)`. This should leave you with the exact route match only, and not the parent routes. – Alex Jan 20 '23 at 23:23
9

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);

  (...)
};
m.wrona
  • 107
  • 1
  • 4
  • 4
    This solution would break when parts of the path have the same value, e.g. `/user/1/badge/1/comment/1`. While that's an uncommon scenario, this solution is still not universally reliable. – Ben Barkay Apr 30 '22 at 12:47
  • 1
    Better to use https://reactrouter.com/docs/en/v6/api#matchroutes instead. – Herman J. Radtke III May 01 '22 at 19:32
  • @HermanJ.RadtkeIII link seems to be broken. Maybe you mean [this](https://reactrouter.com/docs/en/v6/utils/match-routes) – Reimirno Jul 07 '22 at 03:51
  • I used the solution above but with: ```path = path.replace(new RegExp(`\\/${paramValue}(\\/|\\?|$)`), `/:${paramName}$1`);``` It's a little safer as it is less likely to cause an erroneous match on another part of the path that happens to match a param value – DaveF Feb 14 '23 at 00:06
  • @BenBarkay, I'm not sure this is true. Unless you pass the g flag to the replace function, it will only replace the first instance of the match. Since the params entries are enumerated in order, it should successfully substitute even when multiple params have the same value. The bigger risk was that a param value might accidentally match another part of the path. I've tried to tighten this up with my suggested regex above, which requires the param to start with a / and end with either /, ? or the end of the string. It's still not bullet proof though. – DaveF Feb 14 '23 at 09:10
6

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;
}
James M
  • 209
  • 4
  • 10
6

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);
1

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
  );
RichN
  • 6,181
  • 3
  • 30
  • 38
1

Thank @FutianShen. But his/her answer is incorrect if you have nested routes. I tried to fix its issue:

const routes = (
  <Route path='/' element={<Root />}>
    <Route index element={<Index />} />
    <Route path=':orgName' element={<Org />}>
      <Route ...>
        ...
      </Route>
    </Route>
  </Route>
)
const appRoutes = createRoutesFromElements(routes)

function usePathPattern() {
  const location = useLocation()
  return useMemo(
    () =>
      matchRoutes(appRoutes, location)
        .map(({ route: { path } }) => path)
        .filter(Boolean)
        .join('/')
        .replaceAll(/\/\*?\//g, '/'),
    [location],
  )
}

This also works for nested routes.

Sample output:

/:orgName/projects/:projectSlug/vms/:vmUid/*

But if you don't have access to your appRoutes object, I don't have any idea except using UNSAFE_RouteContext (thank @RichN).

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, '/')
}

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 answer).

So this would be enough:

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

function useRoutePattern() {
  const routeContext = useContext(UNSAFE_RouteContext)
  return useMemo(
    () =>
      routeContext.matches
        .map(({ route: { path } }) => path)
        .filter(Boolean)
        .join('/')
        .replaceAll(/\/\*?\//g, '/'),
    [routeContext.matches],
  )
}

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/
Mir-Ismaili
  • 13,974
  • 8
  • 82
  • 100
1

With React Router v6:

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

const routes = ["/", "/login", "product", "product/:id"];

const usePathPattern = (): string | undefined => {
  const { pathname } = useLocation();
  return routes.find((route) => matchPath(route, pathname));
};
Gynekolog
  • 164
  • 1
  • 7
0

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;
}
Tobi
  • 5,499
  • 3
  • 31
  • 47
0

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>
}
Labithiotis
  • 3,519
  • 7
  • 27
  • 47
0

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.

jerryurenaa
  • 3,863
  • 1
  • 27
  • 17
0

For React-Router-Dom version 6 (v6), Use this code:

first import 'useLocation' from RRD.

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

Then make a constant and use it wherever needed.

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

Hope this will resolve your problem of getting path name in React Router Dom version 6

MD ALI
  • 13
  • 2
0

You may use matchPath.

 const { pathName } = useLocation();
    
 const currentRoute = ["/", "/messages", "/messages/:id"].find((pattern) => {
     return matchPath(pattern, pathname);
 });
const useMatchPath = (
  routes: string[],
): string | undefined => {
  const { pathname } = useLocation();
  const [route, setRoute] = useState<string | undefined>(undefined);

  useEffect(() => {
    const currentRoute = routes.find((pattern) => {
      return matchPath(pattern, pathname);
    });
    setRoute(currentRoute);
  }, [pathname]);

  return route;
};

And use it directly inside a Component:

const currentRoute = useMatchPath(["/", "/messages", "/messages/:id"])

Reference:

https://reactrouter.com/en/6.9.0/utils/match-path


Alternatively, you may use, useMatch which is a react-router hook that uses matchPath.

const useMatchRoute = (routes: string[]): string | undefined => {
  return routes.find((route) => useMatch(route));
};


const currentRoute = useMatchRoute(["/", "/messages", "/messages/:id"]) 

Reference:

https://reactrouter.com/en/6.9.0/hooks/use-match

mahan
  • 12,366
  • 5
  • 48
  • 83
0

Check this code:

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

const usePath: any = () => {
  let { pathname } = useLocation();
  const params = useParams();
  return Object.entries(params).reduce((path: any, [key, value]) => {
    return path.replace(`/${value}`, `/:${key}`);
  }, pathname);
};

export default usePath;

enjoy it!

0
<nav className="h-auto grid space-y-5 over-x-auto overflow-y-auto">
          {navLinks.map((item, idx) => (
            <div key={idx} className="grid">
              <label className="uppercase px-5 text-gray-400 text-xs mb-2">
                {item.menu}
              </label>
              {item.submenu.map((sitem, idx) => (
                <NavLink
                  role="button"
                  key={idx}
                  to={`/${item.path}/${sitem.path}`}
                  className={({ isActive, isPending }) =>
                    isPending
                      ? "pending"
                      : isActive
                      ? "inline-flex gap-x-2 py-2 bg-gray-700 px-5 items-center w-full text-gray-300 font-medium"
                      : "inline-flex gap-x-2 py-2 hover:bg-gray-700 px-5 items-center w-full text-gray-400"
                  }
                >
                  <PiChatsLight size={20} />
                  {sitem.name}
                </NavLink>
              ))}
            </div>
          ))}
        </nav>
-1

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.

abc
  • 1,141
  • 12
  • 29
-3
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>
-4

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.

András Geiszl
  • 966
  • 2
  • 16
  • 36