From ecaca0870a5efeb6d4f8ea37d6d447a2bd39f277 Mon Sep 17 00:00:00 2001
From: Manuel Schiller <6340397+schiller-manuel@users.noreply.github.com>
Date: Sat, 10 Oct 2026 11:02:24 +0200
Subject: [PATCH 1/2] test(react-router): cover selector changes without
navigation
---
.../react-router/tests/useLocation.test.tsx | 81 ++++++++++++++++++-
1 file changed, 79 insertions(+), 2 deletions(-)
diff --git a/packages/react-router/tests/useLocation.test.tsx b/packages/react-router/tests/useLocation.test.tsx
index ab3dce78cde..083bd1120fc 100644
--- a/packages/react-router/tests/useLocation.test.tsx
+++ b/packages/react-router/tests/useLocation.test.tsx
@@ -1,6 +1,13 @@
-import { useEffect } from 'react'
+import { useEffect, useState } from 'react'
import { afterEach, describe, expect, test, vi } from 'vitest'
-import { act, cleanup, render, screen, waitFor } from '@testing-library/react'
+import {
+ act,
+ cleanup,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from '@testing-library/react'
import {
Outlet,
RouterProvider,
@@ -9,6 +16,7 @@ import {
createRoute,
createRouter,
useLocation,
+ useRouterState,
} from '../src'
afterEach(() => {
@@ -17,6 +25,75 @@ afterEach(() => {
})
describe('useLocation', () => {
+ test('updates allocating selectors when their captured props change without navigation', async () => {
+ function Selection({ prefix }: { prefix: string }) {
+ const location = useLocation({
+ select: (value) => ({ label: `${prefix}${value.pathname}` }),
+ structuralSharing: false,
+ })
+ const state = useRouterState({
+ select: (value) => ({ label: `${prefix}${value.location.pathname}` }),
+ structuralSharing: false,
+ })
+
+ return (
+ <>
+
{location.label}
+ {state.label}
+ >
+ )
+ }
+
+ const rootRoute = createRootRoute({
+ component: function RootComponent() {
+ const [prefix, setPrefix] = useState('first:')
+
+ return (
+ <>
+
+
+
+ >
+ )
+ },
+ })
+ const postsRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/posts',
+ component: () => Posts
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([postsRoute]),
+ history: createMemoryHistory({ initialEntries: ['/posts'] }),
+ })
+
+ render()
+
+ expect(
+ await screen.findByRole('heading', { name: 'Posts' }),
+ ).toBeInTheDocument()
+ await waitFor(() => expect(router.state.status).toBe('idle'))
+ expect(screen.getByTestId('location-selection')).toHaveTextContent(
+ 'first:/posts',
+ )
+ expect(screen.getByTestId('state-selection')).toHaveTextContent(
+ 'first:/posts',
+ )
+ const initialState = router.state
+
+ fireEvent.click(screen.getByRole('button', { name: 'Change selector' }))
+
+ expect(screen.getByTestId('location-selection')).toHaveTextContent(
+ 'second:/posts',
+ )
+ expect(screen.getByTestId('state-selection')).toHaveTextContent(
+ 'second:/posts',
+ )
+ expect(router.state).toBe(initialState)
+ })
+
test('keeps a selected pathname reference stable across search and hash updates when structural sharing is enabled', async () => {
const effectSpy = vi.fn()
const pathnameSelections: Array<{ pathname: string }> = []
From de0daab7703933dc87b5e529fcaf0e5193d93ce6 Mon Sep 17 00:00:00 2001
From: Manuel Schiller <6340397+schiller-manuel@users.noreply.github.com>
Date: Sat, 10 Oct 2026 11:04:23 +0200
Subject: [PATCH 2/2] chore(react-router): bump react-store to 1.0.0-alpha.0
---
.changeset/wide-shoes-invite.md | 5 +++++
packages/react-router/package.json | 2 +-
pnpm-lock.yaml | 11 +++++------
pnpm-workspace.yaml | 2 ++
4 files changed, 13 insertions(+), 7 deletions(-)
create mode 100644 .changeset/wide-shoes-invite.md
diff --git a/.changeset/wide-shoes-invite.md b/.changeset/wide-shoes-invite.md
new file mode 100644
index 00000000000..79613137de9
--- /dev/null
+++ b/.changeset/wide-shoes-invite.md
@@ -0,0 +1,5 @@
+---
+'@tanstack/react-router': patch
+---
+
+Upgrade `@tanstack/react-store` to `1.0.0-alpha.0` to use React's native `useSyncExternalStore` and remove its external-store shim dependency.
diff --git a/packages/react-router/package.json b/packages/react-router/package.json
index fca9731bdea..e789defb135 100644
--- a/packages/react-router/package.json
+++ b/packages/react-router/package.json
@@ -93,7 +93,7 @@
},
"dependencies": {
"@tanstack/history": "workspace:*",
- "@tanstack/react-store": "^0.11.2",
+ "@tanstack/react-store": "^1.0.0-alpha.0",
"@tanstack/router-core": "workspace:*",
"isbot": "catalog:"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bacfddc7109..c8a10a92c7c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -13914,8 +13914,8 @@ importers:
specifier: workspace:*
version: link:../history
'@tanstack/react-store':
- specifier: ^0.11.2
- version: 0.11.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ specifier: ^1.0.0-alpha.0
+ version: 1.0.0-alpha.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@tanstack/router-core':
specifier: workspace:*
version: link:../router-core
@@ -21728,8 +21728,8 @@ packages:
peerDependencies:
react: ^19.2.3
- '@tanstack/react-store@0.11.2':
- resolution: {integrity: sha512-oj5O5DmiMWE+ih+SioX2ayC1kbK7d3fpLx0B75fPJr9nY3zU+hoW010yr64qXsY/9fyiLVE2bn5mGNV3owm8RQ==}
+ '@tanstack/react-store@1.0.0-alpha.0':
+ resolution: {integrity: sha512-9nP706EWgwrXUERJjE7k50ZgQfnALMgma9fvwiSdN1bGdyJutVB9sVe0/BestCHO0ghiJWJshGn8qFp6uOiNKw==}
peerDependencies:
react: ^19.2.3
react-dom: ^19.2.3
@@ -35749,12 +35749,11 @@ snapshots:
'@tanstack/query-core': 5.102.0
react: 19.2.3
- '@tanstack/react-store@0.11.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
+ '@tanstack/react-store@1.0.0-alpha.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@tanstack/store': 0.11.2
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
- use-sync-external-store: 1.6.0(react@19.2.3)
'@tanstack/react-virtual@3.13.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 5319b48a691..ed2b17d4300 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,5 +1,7 @@
cleanupUnusedCatalogs: true
minimumReleaseAge: 1440
+minimumReleaseAgeExclude:
+ - '@tanstack/react-store@1.0.0-alpha.0'
linkWorkspacePackages: true
preferWorkspacePackages: true
blockExoticSubdeps: true