diff --git a/packages/docs/src/pages/ApiHooksPage.tsx b/packages/docs/src/pages/ApiHooksPage.tsx
index 79dbbce..1a2d140 100644
--- a/packages/docs/src/pages/ApiHooksPage.tsx
+++ b/packages/docs/src/pages/ApiHooksPage.tsx
@@ -21,7 +21,16 @@ function MyComponent() {
console.log(location.pathname); // "/users/123"
console.log(location.search); // "?tab=profile"
console.log(location.hash); // "#section"
+ console.log(location.entryId); // NavigationHistoryEntry.id or null
+ console.log(location.entryKey); // NavigationHistoryEntry.key or null
}`}
+
+ entryId and entryKey are null{" "}
+ when the Navigation API is unavailable. Do not render them directly in
+ DOM — they are not available during SSR and will cause a hydration
+ mismatch. Use them as a React key or in effects/callbacks
+ instead.
+
diff --git a/packages/docs/src/pages/ApiTypesPage.tsx b/packages/docs/src/pages/ApiTypesPage.tsx
index 3bbbe02..0f8e814 100644
--- a/packages/docs/src/pages/ApiTypesPage.tsx
+++ b/packages/docs/src/pages/ApiTypesPage.tsx
@@ -335,7 +335,24 @@ routeState<{ tab: string }>()({
pathname: string;
search: string;
hash: string;
+ entryId: string | null; // NavigationHistoryEntry.id
+ entryKey: string | null; // NavigationHistoryEntry.key
}`}
+
+ entryId and entryKey expose the
+ corresponding properties from the Navigation API's{" "}
+ NavigationHistoryEntry. entryId is a unique
+ identifier for the entry — a new id is assigned when the entry is
+ replaced. entryKey represents the slot in the entry list
+ and is stable across replacements. Both are null when the
+ Navigation API is unavailable (e.g., in static fallback mode).
+
+
+ Warning: 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 key or
+ in effects/callbacks.
+
diff --git a/packages/router/src/Router/index.tsx b/packages/router/src/Router/index.tsx
index 9511667..8273477 100644
--- a/packages/router/src/Router/index.tsx
+++ b/packages/router/src/Router/index.tsx
@@ -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;
+ const entryKey =
+ locationEntry?.entryKey ??
+ (isServerSnapshot(locationEntryInternal)
+ ? locationEntryInternal.actualLocationEntry?.entryKey
+ : null) ??
+ null;
const routerContextValue: RouterContextValue = useMemo(
() => ({
locationState,
locationInfo,
url: urlObject,
+ entryId,
+ entryKey,
isPending,
navigateAsync,
updateCurrentEntryState,
@@ -276,6 +290,8 @@ export function Router({
locationState,
locationInfo,
urlObject,
+ entryId,
+ entryKey,
isPending,
navigateAsync,
updateCurrentEntryState,
diff --git a/packages/router/src/__tests__/fallback.test.tsx b/packages/router/src/__tests__/fallback.test.tsx
index 9cb8920..0b0d914 100644
--- a/packages/router/src/__tests__/fallback.test.tsx
+++ b/packages/router/src/__tests__/fallback.test.tsx
@@ -143,6 +143,26 @@ describe("Fallback Mode", () => {
expect(screen.getByTestId("hash").textContent).toBe("#section");
});
+ it("returns null for entryId and entryKey when Navigation API is unavailable", () => {
+ setupStaticLocation("http://localhost/");
+
+ function Page() {
+ const location = useLocation();
+ return (
+
+ {location.entryId ?? "null"}
+ {location.entryKey ?? "null"}
+
+ );
+ }
+
+ const routes: RouteDefinition[] = [{ path: "/", component: Page }];
+
+ render();
+ expect(screen.getByTestId("entryId").textContent).toBe("null");
+ expect(screen.getByTestId("entryKey").textContent).toBe("null");
+ });
+
it("renders nothing when no route matches", () => {
setupStaticLocation("http://localhost/unknown");
@@ -431,6 +451,24 @@ describe("ssr", () => {
expect(screen.getByTestId("hash").textContent).toBe("");
});
+ it("returns null for entryId and entryKey during SSR", () => {
+ function Page() {
+ const location = useLocation();
+ return (
+
+ {location.entryId ?? "null"}
+ {location.entryKey ?? "null"}
+
+ );
+ }
+
+ const routes: RouteDefinition[] = [{ path: "/about", component: Page }];
+
+ render();
+ expect(screen.getByTestId("entryId").textContent).toBe("null");
+ expect(screen.getByTestId("entryKey").textContent).toBe("null");
+ });
+
it("pathless route wrapping path-based children works with ssr.path", () => {
const routes: RouteDefinition[] = [
{
diff --git a/packages/router/src/__tests__/hooks.test.tsx b/packages/router/src/__tests__/hooks.test.tsx
index 369fbd1..4731939 100644
--- a/packages/router/src/__tests__/hooks.test.tsx
+++ b/packages/router/src/__tests__/hooks.test.tsx
@@ -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 (
+
+ {location.entryId ?? "null"}
+ {location.entryKey ?? "null"}
+
+ );
+ }
+
+ const routes: RouteDefinition[] = [
+ { path: "/", component: TestComponent },
+ ];
+
+ render();
+
+ // 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");
+ });
+
it("throws when used outside Router", () => {
function TestComponent() {
useLocation();
diff --git a/packages/router/src/context/RouterContext.ts b/packages/router/src/context/RouterContext.ts
index 3f04d00..f41286f 100644
--- a/packages/router/src/context/RouterContext.ts
+++ b/packages/router/src/context/RouterContext.ts
@@ -14,6 +14,10 @@ export type RouterContextValue = {
locationInfo: unknown;
/** Current URL (null during SSR) */
url: URL | null;
+ /** NavigationHistoryEntry.id — unique identifier for this entry. A new id is assigned when the entry is replaced. Null during SSR or when Navigation API is unavailable. */
+ entryId: string | null;
+ /** NavigationHistoryEntry.key — represents the slot in the entry list. Stable across replacements. Null during SSR or when Navigation API is unavailable. */
+ entryKey: string | null;
/** Whether a navigation transition is pending */
isPending: boolean;
/** Navigate to a new URL and wait for completion */
diff --git a/packages/router/src/core/NavigationAPIAdapter.ts b/packages/router/src/core/NavigationAPIAdapter.ts
index 92c27ab..4e30b6a 100644
--- a/packages/router/src/core/NavigationAPIAdapter.ts
+++ b/packages/router/src/core/NavigationAPIAdapter.ts
@@ -64,6 +64,8 @@ export class NavigationAPIAdapter implements RouterAdapter {
this.#cachedSnapshot = {
url: new URL(entry.url),
key: this.#effectiveKey(entry.id),
+ entryId: entry.id,
+ entryKey: entry.key,
state: entry.getState(),
info: this.#currentNavigationInfo,
};
diff --git a/packages/router/src/core/RouterAdapter.ts b/packages/router/src/core/RouterAdapter.ts
index 9dfaf43..414e1fe 100644
--- a/packages/router/src/core/RouterAdapter.ts
+++ b/packages/router/src/core/RouterAdapter.ts
@@ -20,6 +20,10 @@ export type LocationEntry = {
url: URL;
/** Unique key for this entry (used for loader caching) */
key: string;
+ /** NavigationHistoryEntry.id — unique identifier for this entry. A new id is assigned when the entry is replaced. */
+ entryId: string | null;
+ /** NavigationHistoryEntry.key — represents the slot in the entry list. Stable across replacements. */
+ entryKey: string | null;
/** State associated with this entry */
state: unknown;
/** Ephemeral info from current navigation (undefined if not from navigation event) */
diff --git a/packages/router/src/core/StaticAdapter.ts b/packages/router/src/core/StaticAdapter.ts
index d85458d..5c111cc 100644
--- a/packages/router/src/core/StaticAdapter.ts
+++ b/packages/router/src/core/StaticAdapter.ts
@@ -28,6 +28,8 @@ export class StaticAdapter implements RouterAdapter {
this.#cachedSnapshot = {
url: new URL(window.location.href),
key: "__static__",
+ entryId: null,
+ entryKey: null,
state: undefined,
info: undefined,
};
diff --git a/packages/router/src/hooks/useLocation.ts b/packages/router/src/hooks/useLocation.ts
index 41ed04f..dc2c83f 100644
--- a/packages/router/src/hooks/useLocation.ts
+++ b/packages/router/src/hooks/useLocation.ts
@@ -12,7 +12,7 @@ export function useLocation(): Location {
throw new Error("useLocation must be used within a Router");
}
- const { url } = context;
+ const { url, entryId, entryKey } = context;
if (url === null) {
throw new Error("useLocation: URL is not available during SSR.");
@@ -23,6 +23,8 @@ export function useLocation(): Location {
pathname: url.pathname,
search: url.search,
hash: url.hash,
+ entryId,
+ entryKey,
};
- }, [url]);
+ }, [url, entryId, entryKey]);
}
diff --git a/packages/router/src/types.ts b/packages/router/src/types.ts
index 366e5eb..f902410 100644
--- a/packages/router/src/types.ts
+++ b/packages/router/src/types.ts
@@ -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.
+ */
+ entryKey: string | null;
};
/**