feat: expose entryId and entryKey from Navigation API in useLocation#151
feat: expose entryId and entryKey from Navigation API in useLocation#151
Conversation
Add NavigationHistoryEntry.id and NavigationHistoryEntry.key to the Location type returned by useLocation(). These are exposed as entryId and entryKey respectively, and are null when the Navigation API is unavailable (e.g., static fallback mode). https://claude.ai/code/session_014uqruYL8PktVwDVrSA6HJQ
entryId is unique per entry and changes on replacement. entryKey represents the slot and is stable across replacements. Also warns against rendering these values in DOM since they are unavailable during SSR. https://claude.ai/code/session_014uqruYL8PktVwDVrSA6HJQ
Use the same pattern as locationKey to inspect the server snapshot's actual location entry during the first hydration render. https://claude.ai/code/session_014uqruYL8PktVwDVrSA6HJQ
There was a problem hiding this comment.
Pull request overview
This PR extends the router’s useLocation() API to expose Navigation API NavigationHistoryEntry.id and .key via two new Location fields: entryId and entryKey, using null when the Navigation API isn’t available (e.g., fallback/static mode).
Changes:
- Extend
Location/ router context and adapter snapshot types to includeentryIdandentryKey. - Plumb
entryId/entryKeyfrom adapters →RouterContext→useLocation(). - Add docs and a hook test asserting
entryId/entryKeyare present under the Navigation API mock.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/router/src/types.ts | Adds entryId/entryKey to the public Location type with SSR/hydration warnings. |
| packages/router/src/hooks/useLocation.ts | Returns entryId/entryKey from RouterContext and memoizes with updated deps. |
| packages/router/src/core/StaticAdapter.ts | Sets entryId/entryKey to null in static fallback snapshots. |
| packages/router/src/core/RouterAdapter.ts | Extends internal LocationEntry snapshot type with entryId/entryKey. |
| packages/router/src/core/NavigationAPIAdapter.ts | Populates snapshot entryId/entryKey from navigation.currentEntry. |
| packages/router/src/context/RouterContext.ts | Adds entryId/entryKey to the context value contract. |
| packages/router/src/tests/hooks.test.tsx | Adds a test for useLocation() returning entryId/entryKey under Navigation API mock. |
| packages/router/src/Router/index.tsx | Plumbs entryId/entryKey into RouterContext from the current location entry. |
| packages/docs/src/pages/ApiTypesPage.tsx | Documents the new Location.entryId/entryKey fields and SSR warning. |
| packages/docs/src/pages/ApiHooksPage.tsx | Documents useLocation() exposing entryId/entryKey and SSR warning. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| it("returns entryId and entryKey from Navigation API", () => { | ||
| function TestComponent() { | ||
| const location = useLocation(); | ||
| return ( | ||
| <div> | ||
| <span data-testid="entryId">{location.entryId ?? "null"}</span> | ||
| <span data-testid="entryKey">{location.entryKey ?? "null"}</span> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| const routes: RouteDefinition[] = [ | ||
| { path: "/", component: TestComponent }, | ||
| ]; | ||
|
|
||
| render(<Router routes={routes} />); | ||
|
|
||
| // The mock navigation generates UUIDs for id and key | ||
| expect(screen.getByTestId("entryId").textContent).not.toBe("null"); | ||
| expect(screen.getByTestId("entryKey").textContent).not.toBe("null"); | ||
| }); |
There was a problem hiding this comment.
Test coverage currently only asserts that entryId/entryKey are non-null with the Navigation API mock. Since the public contract says these are null when the Navigation API is unavailable and during SSR/hydration, it would be good to add assertions for those cases as well (e.g., static fallback mode and an SSR+hydrate scenario) to prevent regressions—especially given the hydration-mismatch warning in the docs.
| const entryId = | ||
| locationEntry?.entryId ?? | ||
| (isServerSnapshot(locationEntryInternal) | ||
| ? locationEntryInternal.actualLocationEntry?.entryId | ||
| : null) ?? | ||
| null; |
There was a problem hiding this comment.
entryId is being populated from ServerLocationSnapshot.actualLocationEntry during hydration (when locationEntryInternal is a server snapshot). That makes entryId non-null on the first client render even though it was null on the server, which violates the documented “null during SSR/hydration” contract and can trigger hydration/key mismatches. Consider returning null for entryId while isServerSnapshot(locationEntryInternal) (and only exposing it once locationEntryInternal has synced to the real client snapshot).
| const entryId = | |
| locationEntry?.entryId ?? | |
| (isServerSnapshot(locationEntryInternal) | |
| ? locationEntryInternal.actualLocationEntry?.entryId | |
| : null) ?? | |
| null; | |
| const entryId = isServerSnapshot(locationEntryInternal) | |
| ? null | |
| : locationEntry?.entryId ?? null; |
| const entryKey = | ||
| locationEntry?.entryKey ?? | ||
| (isServerSnapshot(locationEntryInternal) | ||
| ? locationEntryInternal.actualLocationEntry?.entryKey | ||
| : null) ?? | ||
| null; |
There was a problem hiding this comment.
Same issue as entryId: entryKey is read from ServerLocationSnapshot.actualLocationEntry during hydration, so it can differ between server HTML and the first client render. Since this value is explicitly documented as unavailable during SSR, it should stay null while locationEntryInternal is a server snapshot to avoid hydration/key mismatches.
| const entryKey = | |
| locationEntry?.entryKey ?? | |
| (isServerSnapshot(locationEntryInternal) | |
| ? locationEntryInternal.actualLocationEntry?.entryKey | |
| : null) ?? | |
| null; | |
| const entryKey = isServerSnapshot(locationEntryInternal) | |
| ? null | |
| : locationEntry?.entryKey ?? null; |
| * Null when Navigation API is unavailable. | ||
| * | ||
| * **Warning:** Do not render this value directly in DOM, as it is not | ||
| * available during SSR and will cause a hydration mismatch. Use it as a | ||
| * React `key` or in effects/callbacks instead. | ||
| */ | ||
| entryId: string | null; | ||
| /** | ||
| * NavigationHistoryEntry.key — represents the slot in the entry list. | ||
| * Stable across replacements. | ||
| * Null when Navigation API is unavailable. | ||
| * | ||
| * **Warning:** Do not render this value directly in DOM, as it is not | ||
| * available during SSR and will cause a hydration mismatch. Use it as a | ||
| * React `key` or in effects/callbacks instead. |
There was a problem hiding this comment.
The docstring says “Null when Navigation API is unavailable”, but the warning immediately below references SSR/hydration. Consider updating the main description to also state “null during SSR/hydration”, and reconsider the suggestion to “Use it as a React key”: keys participate in reconciliation/hydration, so a value that’s null on the server and non-null on the client can still cause hydration issues even if it’s not rendered to the DOM.
| * Null when Navigation API is unavailable. | |
| * | |
| * **Warning:** Do not render this value directly in DOM, as it is not | |
| * available during SSR and will cause a hydration mismatch. Use it as a | |
| * React `key` or in effects/callbacks instead. | |
| */ | |
| entryId: string | null; | |
| /** | |
| * NavigationHistoryEntry.key — represents the slot in the entry list. | |
| * Stable across replacements. | |
| * Null when Navigation API is unavailable. | |
| * | |
| * **Warning:** Do not render this value directly in DOM, as it is not | |
| * available during SSR and will cause a hydration mismatch. Use it as a | |
| * React `key` or in effects/callbacks instead. | |
| * Null when Navigation API is unavailable or during SSR/hydration. | |
| * | |
| * **Warning:** Do not render this value directly in DOM, as it is not | |
| * available during SSR and will cause a hydration mismatch. Avoid using it | |
| * as a React `key` in SSR/hydrated trees; prefer using it only in | |
| * effects/callbacks or other client-only logic instead. | |
| */ | |
| entryId: string | null; | |
| /** | |
| * NavigationHistoryEntry.key — represents the slot in the entry list. | |
| * Stable across replacements. | |
| * Null when Navigation API is unavailable or during SSR/hydration. | |
| * | |
| * **Warning:** Do not render this value directly in DOM, as it is not | |
| * available during SSR and will cause a hydration mismatch. Avoid using it | |
| * as a React `key` in SSR/hydrated trees; prefer using it only in | |
| * effects/callbacks or other client-only logic instead. |
| <strong>Warning:</strong> Do not render these values directly in DOM, | ||
| as they are not available during SSR and will cause a hydration | ||
| mismatch. They are best suited for use as a React <code>key</code> or | ||
| in effects/callbacks. |
There was a problem hiding this comment.
This warning suggests these values are “best suited for use as a React key”, but if entryId/entryKey are null during SSR and become non-null on the client, using them as keys in SSR-rendered trees can still cause hydration/reconciliation issues. Consider adjusting the guidance to emphasize use in effects/callbacks (or only in client-only renders) rather than as keys in SSR output.
| <strong>Warning:</strong> Do not render these values directly in DOM, | |
| as they are not available during SSR and will cause a hydration | |
| mismatch. They are best suited for use as a React <code>key</code> or | |
| in effects/callbacks. | |
| <strong>Warning:</strong> Do not render these values directly into the | |
| DOM or use them as React <code>key</code> props in trees that are | |
| server-rendered, as they are not available during SSR and will cause | |
| hydration mismatches. Prefer using them in effects/callbacks or in | |
| client-only components. |
| mismatch. Use them as a React <code>key</code> or in effects/callbacks | ||
| instead. |
There was a problem hiding this comment.
The text recommends using entryId/entryKey as a React key, but because these values are unavailable during SSR, using them as keys in SSR-rendered output can still produce hydration/reconciliation problems when they change on the client. Consider narrowing the recommendation to effects/callbacks (or only client-only rendering) instead of keys.
| mismatch. Use them as a React <code>key</code> or in effects/callbacks | |
| instead. | |
| mismatch. Use them in effects or callbacks instead. |
Covers the cases where Navigation API is unavailable (static fallback mode) and during SSR, verifying entryId and entryKey are null. https://claude.ai/code/session_014uqruYL8PktVwDVrSA6HJQ
Add NavigationHistoryEntry.id and NavigationHistoryEntry.key to the
Location type returned by useLocation(). These are exposed as entryId
and entryKey respectively, and are null when the Navigation API is
unavailable (e.g., static fallback mode).
https://claude.ai/code/session_014uqruYL8PktVwDVrSA6HJQ