@tanstack/react-router 1.69.1 → 1.70.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-router",
3
- "version": "1.69.1",
3
+ "version": "1.70.1",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
package/src/Matches.tsx CHANGED
@@ -142,7 +142,6 @@ export interface RouteMatch<
142
142
  paramsError: unknown
143
143
  searchError: unknown
144
144
  updatedAt: number
145
- componentsPromise?: Promise<Array<void>>
146
145
  loadPromise?: ControlledPromise<void>
147
146
  beforeLoadPromise?: ControlledPromise<void>
148
147
  loaderPromise?: ControlledPromise<void>
@@ -39,6 +39,7 @@ export function Transitioner() {
39
39
  params: true,
40
40
  hash: true,
41
41
  state: true,
42
+ _includeValidateSearch: true,
42
43
  })
43
44
 
44
45
  if (
package/src/link.tsx CHANGED
@@ -595,12 +595,7 @@ export function useLinkProps<
595
595
  activeProps = () => ({ className: 'active' }),
596
596
  inactiveProps = () => ({}),
597
597
  activeOptions,
598
- hash,
599
- search,
600
- params,
601
598
  to,
602
- state,
603
- mask,
604
599
  preload: userPreload,
605
600
  preloadDelay: userPreloadDelay,
606
601
  replace,
@@ -636,9 +631,12 @@ export function useLinkProps<
636
631
  return 'internal'
637
632
  }, [to])
638
633
 
634
+ // subscribe to search params to re-build location if it changes
635
+ const currentSearch = useRouterState({ select: (s) => s.location.search })
636
+
639
637
  const next = React.useMemo(
640
638
  () => router.buildLocation(options as any),
641
- [router, options],
639
+ [router, options, currentSearch],
642
640
  )
643
641
  const preload = React.useMemo(
644
642
  () => userPreload ?? router.options.defaultPreload,
package/src/route.ts CHANGED
@@ -945,6 +945,7 @@ export class Route<
945
945
  rank!: number
946
946
  lazyFn?: () => Promise<LazyRoute<any>>
947
947
  _lazyPromise?: Promise<void>
948
+ _componentsPromise?: Promise<Array<void>>
948
949
 
949
950
  /**
950
951
  * @deprecated Use the `createRoute` function instead.
package/src/router.ts CHANGED
@@ -447,11 +447,21 @@ export interface RouterOptions<
447
447
  * While usually automatic, sometimes it can be useful to force the router into a server-side state, e.g. when using the router in a non-browser environment that has access to a global.document object.
448
448
  *
449
449
  * @default typeof document !== 'undefined'
450
- * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#isserver property)
450
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#isserver-property)
451
451
  */
452
452
  isServer?: boolean
453
453
 
454
454
  defaultSsr?: boolean
455
+
456
+ search?: {
457
+ /**
458
+ * Configures how unknown search params (= not returned by any `validateSearch`) are treated.
459
+ *
460
+ * @default false
461
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#search.strict-property)
462
+ */
463
+ strict?: boolean
464
+ }
455
465
  }
456
466
 
457
467
  export interface RouterErrorSerializer<TSerializedError> {
@@ -1511,6 +1521,7 @@ export class Router<
1511
1521
  let search = applyMiddlewares()
1512
1522
 
1513
1523
  if (opts._includeValidateSearch) {
1524
+ let validatedSearch = this.options.search?.strict ? {} : search
1514
1525
  matchedRoutesResult?.matchedRoutes.forEach((route) => {
1515
1526
  try {
1516
1527
  if (route.options.validateSearch) {
@@ -1518,12 +1529,16 @@ export class Router<
1518
1529
  typeof route.options.validateSearch === 'object'
1519
1530
  ? route.options.validateSearch.parse
1520
1531
  : route.options.validateSearch
1521
- search = { ...search, ...validator(search) }
1532
+ validatedSearch = {
1533
+ ...validatedSearch,
1534
+ ...validator({ ...validatedSearch, ...search }),
1535
+ }
1522
1536
  }
1523
1537
  } catch (e) {
1524
1538
  // ignore errors here because they are already handled in matchRoutes
1525
1539
  }
1526
1540
  })
1541
+ search = validatedSearch
1527
1542
  }
1528
1543
  search = replaceEqualDeep(fromSearch, search)
1529
1544
  const searchStr = this.options.stringifySearch(search)
@@ -2336,37 +2351,41 @@ export class Router<
2336
2351
 
2337
2352
  // Actually run the loader and handle the result
2338
2353
  try {
2339
- route._lazyPromise =
2340
- route._lazyPromise ||
2341
- (route.lazyFn
2342
- ? route.lazyFn().then((lazyRoute) => {
2343
- const { id, ...options } = lazyRoute.options
2354
+ if (route._lazyPromise === undefined) {
2355
+ if (route.lazyFn) {
2356
+ route._lazyPromise = route
2357
+ .lazyFn()
2358
+ .then((lazyRoute) => {
2359
+ // explicitly don't copy over the lazy route's id
2360
+ const { id: _id, ...options } =
2361
+ lazyRoute.options
2344
2362
  Object.assign(route.options, options)
2345
2363
  })
2346
- : Promise.resolve())
2364
+ } else {
2365
+ route._lazyPromise = Promise.resolve()
2366
+ }
2367
+ }
2347
2368
 
2348
2369
  // If for some reason lazy resolves more lazy components...
2349
2370
  // We'll wait for that before pre attempt to preload any
2350
2371
  // components themselves.
2351
- const componentsPromise =
2352
- this.getMatch(matchId)!.componentsPromise ||
2353
- route._lazyPromise.then(() =>
2354
- Promise.all(
2355
- componentTypes.map(async (type) => {
2356
- const component = route.options[type]
2357
-
2358
- if ((component as any)?.preload) {
2359
- await (component as any).preload()
2360
- }
2361
- }),
2362
- ),
2372
+ if (route._componentsPromise === undefined) {
2373
+ route._componentsPromise = route._lazyPromise.then(
2374
+ () =>
2375
+ Promise.all(
2376
+ componentTypes.map(async (type) => {
2377
+ const component = route.options[type]
2378
+ if ((component as any)?.preload) {
2379
+ await (component as any).preload()
2380
+ }
2381
+ }),
2382
+ ),
2363
2383
  )
2384
+ }
2364
2385
 
2365
- // Otherwise, load the route
2366
2386
  updateMatch(matchId, (prev) => ({
2367
2387
  ...prev,
2368
2388
  isFetching: 'loader',
2369
- componentsPromise,
2370
2389
  }))
2371
2390
 
2372
2391
  // Kick off the loader!
@@ -2442,9 +2461,9 @@ export class Router<
2442
2461
  }))
2443
2462
  }
2444
2463
 
2445
- // Last but not least, wait for the the component
2464
+ // Last but not least, wait for the the components
2446
2465
  // to be preloaded before we resolve the match
2447
- await this.getMatch(matchId)!.componentsPromise
2466
+ await route._componentsPromise
2448
2467
  } catch (err) {
2449
2468
  handleRedirectAndNotFound(this.getMatch(matchId)!, err)
2450
2469
  }
@@ -2505,14 +2524,21 @@ export class Router<
2505
2524
  return matches
2506
2525
  }
2507
2526
 
2508
- invalidate = () => {
2509
- const invalidate = (d: MakeRouteMatch<TRouteTree>) => ({
2510
- ...d,
2511
- invalid: true,
2512
- ...(d.status === 'error'
2513
- ? ({ status: 'pending', error: undefined } as const)
2514
- : {}),
2515
- })
2527
+ invalidate = (opts?: {
2528
+ filter?: (d: MakeRouteMatch<TRouteTree>) => boolean
2529
+ }) => {
2530
+ const invalidate = (d: MakeRouteMatch<TRouteTree>) => {
2531
+ if (opts?.filter?.(d) ?? true) {
2532
+ return {
2533
+ ...d,
2534
+ invalid: true,
2535
+ ...(d.status === 'error'
2536
+ ? ({ status: 'pending', error: undefined } as const)
2537
+ : {}),
2538
+ }
2539
+ }
2540
+ return d
2541
+ }
2516
2542
 
2517
2543
  this.__store.setState((s) => ({
2518
2544
  ...s,