A Complete Analysis of React Router’s useNavigate Hook - “It’s just one navigate(‘..’), so why does it jump up two levels?”
Analyzing How react-router’s useNavigate Hook Works - Where Does navigate Start From?
Environment
- react-router v7.5
One-line Summary
Where navigate('.') starts calculating the path from depends on the <Route> (= pathnameBase) that the component calling useNavigate() belongs to.
The Symptom
On a detail page (path: /list/:no), I ran the code below to go back to the list (path: /list).
const navigate = useNavigate();
navigate('..'); // expected: /list
But instead of going to /list, it ended up navigating to the root path (path: /). I wondered whether navigate had run twice, so I added a console log to check, but navigate was called exactly once.
Even when I passed the relative: path option as shown below, the result was the same.
navigate('..', { relative: 'path' })
Every other component that uses the same code shows the same behavior.
The Debugging Process
- Number of navigate calls
- What to check: First, suspect that it’s being called twice due to event bubbling or a double click
- How to check: Use console.count()
- Result: Confirmed it was called only once
- Route structure
- What to check: Is the route declaration wrong? Check for absolute vs. relative paths, and whether a pathless wrapper (Route path=””) exists
- How to check: Inspect the
<Route path="...">declarations - Result: No problem
- Current URL
- What to check: Is the URL right before the click definitely
/list/:no? Did I unknowingly redirect to/list? - How to check: Log
location.pathname - Result: No problem. The URL right before the click was
/list/:no.
- What to check: Is the URL right before the click definitely
- Comparing relative paths
- What to check: Compare the behavior of navigate(‘.’) vs navigate(‘..’)
- Result: Contrary to what the official docs describe, navigate(‘.’) worked exactly as expected
- Question: ‘.’ worked as expected, but ‘..’ did not. Isn’t ‘.’ supposed to navigate to the current route, and ‘..’ to the parent route?
- Checking the official docs: The official docs only describe the starting point vaguely. They say the current location is where
useNavigatewas called, but it’s not clear whether that applies only when therelative: "path"option is set, or always.
- Inspecting the useNavigate internals
- What to check: How useNavigate works internally in the
react-router hooks.tsxfile - How to check:
- Result: The calculation reference point for
route navigationis the last element (Route) of the matches array in the current component.
- What to check: How useNavigate works internally in the
- The route reference point
- What to check: Use useMatches() to check the
pathnameBaseof the component that runs useNavigation - Result:
pathnameBaseshows up as/list(!) - Question: Why isn’t
pathnameBase/list/:no?
- What to check: Use useMatches() to check the
The Cause
-
useNavigate() uses the
pathnameBaseof the<Route>that the component it’s declared in belongs to as its reference point. -
Even if you pass relative: ‘path’ in the
navigateoptions, the calculation reference point does not change. It only changes the interpretation method to the “URL segment” approach. -
If you call
useNavigatefrom a higher-level route like a Layout and navigate, then even if you’re looking at a detail page (/list/:no), the reference point becomes/list.
<Route path="/list" element={<Layout />}> {/* ← useNavigate() inside Layout */}
<Route index element={<List />} />
<Route path=":no" element={<Content />} />
</Route>
- Therefore, even applying
'..'just once turns/list->/./list/:nowas never part of the calculation in the first place. As a result, it looks as if two segments were cut off at once.
How to Check the Current Path That useNavigate Recognizes
Method 1: The simplest
In React DevTools, select the component that calls useNavigate -> in the hooks panel, check Navigate.Route.matches[last].pathnameBase
Method 2
const Component = () => {
// ...
// the array of matched routes (root → deepest)
const matches = useMatches();
// the route the current component belongs to = the last element of the array
const current = matches[matches.length - 1];
console.log('pathnameBase : ', current.pathnameBase); // e.g. "/list"
// ...
}
Method 3
// a handy method gpt told me about
import { useResolvedPath } from 'react-router-dom';
function usePathnameBase(): string {
// the result of resolving "." (route-relative) is the pathnameBase
const { pathname } = useResolvedPath('.');
return pathname; // e.g. "/list"
}
Conclusion and Best Practices
- The
pathnameBaseof the component that callsuseNavigateis always the routing reference point. - When you’re confused, using an absolute path for routing from the start is also a good approach
- e.g. navigate(‘/list’)
- When debugging routing, you need to debug the routing reference point.
- Check
pathnameBasewithuseMatches()/useResolvedPath('.')
- Check
Summary
-
Problem: I used
navigate('..)but it goes up more levels than expected. -
Cause: Because navigate’s starting point is the
<Route>(pathnameBase) of the component that runs navigate. If the code that runsnavigateis attached to a higher-level route, like a shared header or search bar, the detail URL segment doesn’t get included in the calculation. - Solutions
- In a shared component on a higher-level route, use navigate(‘.’) to navigate to the parent route, or
- use an absolute path like navigate(‘/notice’), or
- move the
useNavigate()call down to a lower-level route, then runnavigate('..')(to navigate to the parent route)
- Checkpoint: Always check pathnameBase via logs
EOD
20250430
Leave a comment