-
Notifications
You must be signed in to change notification settings - Fork 1
feat: expose entryId and entryKey from Navigation API in useLocation #151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
003ccb9
14933e7
f5f2b66
6f777c3
3761f0b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -335,7 +335,24 @@ routeState<{ tab: string }>()({ | |||||||||||||||||||
| pathname: string; | ||||||||||||||||||||
| search: string; | ||||||||||||||||||||
| hash: string; | ||||||||||||||||||||
| entryId: string | null; // NavigationHistoryEntry.id | ||||||||||||||||||||
| entryKey: string | null; // NavigationHistoryEntry.key | ||||||||||||||||||||
| }`}</CodeBlock> | ||||||||||||||||||||
| <p> | ||||||||||||||||||||
| <code>entryId</code> and <code>entryKey</code> expose the | ||||||||||||||||||||
| corresponding properties from the Navigation API's{" "} | ||||||||||||||||||||
| <code>NavigationHistoryEntry</code>. <code>entryId</code> is a unique | ||||||||||||||||||||
| identifier for the entry — a new id is assigned when the entry is | ||||||||||||||||||||
| replaced. <code>entryKey</code> represents the slot in the entry list | ||||||||||||||||||||
| and is stable across replacements. Both are <code>null</code> when the | ||||||||||||||||||||
| Navigation API is unavailable (e.g., in static fallback mode). | ||||||||||||||||||||
| </p> | ||||||||||||||||||||
| <p> | ||||||||||||||||||||
| <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. | ||||||||||||||||||||
|
Comment on lines
+351
to
+354
|
||||||||||||||||||||
| <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. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -263,11 +263,25 @@ export function Router({ | |||||||||||||||||||
|
|
||||||||||||||||||||
| const locationState = locationEntry?.state; | ||||||||||||||||||||
| const locationInfo = locationEntry?.info; | ||||||||||||||||||||
| const entryId = | ||||||||||||||||||||
| locationEntry?.entryId ?? | ||||||||||||||||||||
| (isServerSnapshot(locationEntryInternal) | ||||||||||||||||||||
| ? locationEntryInternal.actualLocationEntry?.entryId | ||||||||||||||||||||
| : null) ?? | ||||||||||||||||||||
| null; | ||||||||||||||||||||
|
Comment on lines
+266
to
+271
|
||||||||||||||||||||
| const entryId = | |
| locationEntry?.entryId ?? | |
| (isServerSnapshot(locationEntryInternal) | |
| ? locationEntryInternal.actualLocationEntry?.entryId | |
| : null) ?? | |
| null; | |
| const entryId = isServerSnapshot(locationEntryInternal) | |
| ? null | |
| : locationEntry?.entryId ?? null; |
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,6 +47,28 @@ describe("hooks", () => { | |
| expect(screen.getByTestId("hash").textContent).toBe("#section"); | ||
| }); | ||
|
|
||
| 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"); | ||
| }); | ||
|
Comment on lines
+50
to
+70
|
||
|
|
||
| it("throws when used outside Router", () => { | ||
| function TestComponent() { | ||
| useLocation(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -123,6 +123,26 @@ export type Location = { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pathname: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| search: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hash: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * NavigationHistoryEntry.id — unique identifier for this entry. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * A new id is assigned when the entry is replaced. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 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. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+129
to
+143
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The text recommends using
entryId/entryKeyas a Reactkey, 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.