@tanstack/react-router 1.131.26 → 1.131.28

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.
@@ -2866,7 +2866,7 @@ type ToOptions<
2866
2866
  TTo extends string = '',
2867
2867
  > = {
2868
2868
  // \`from\` is an optional route ID or path. If it is not supplied, only absolute paths will be auto-completed and type-safe. It's common to supply the route.fullPath of the origin route you are rendering from for convenience. If you don't know the origin route, leave this empty and work with absolute paths or unsafe relative paths.
2869
- from: string
2869
+ from?: string
2870
2870
  // \`to\` can be an absolute route path or a relative path from the \`from\` option to a valid route path. ⚠️ Do not interpolate path params, hash or search params into the \`to\` options. Use the \`params\`, \`search\`, and \`hash\` options instead.
2871
2871
  to: string
2872
2872
  // \`params\` is either an object of path params to interpolate into the \`to\` option or a function that supplies the previous params and allows you to return new ones. This is the only way to interpolate dynamic parameters into the final URL. Depending on the \`from\` and \`to\` route, you may need to supply none, some or all of the path params. TypeScript will notify you of the required params if there are any.
@@ -3021,7 +3021,7 @@ Keep in mind that normally dynamic segment params are \`string\` values, but the
3021
3021
 
3022
3022
  By default, all links are absolute unless a \`from\` route path is provided. This means that the above link will always navigate to the \`/about\` route regardless of what route you are currently on.
3023
3023
 
3024
- If you want to make a link that is relative to the current route, you can provide a \`from\` route path:
3024
+ Relative links can be combined with a \`from\` route path. If a from route path isn't provided, relative paths default to the current active location.
3025
3025
 
3026
3026
  \`\`\`tsx
3027
3027
  const postIdRoute = createRoute({
@@ -3039,9 +3039,9 @@ As seen above, it's common to provide the \`route.fullPath\` as the \`from\` rou
3039
3039
 
3040
3040
  ### Special relative paths: \`"."\` and \`".."\`
3041
3041
 
3042
- Quite often you might want to reload the current location, for example, to rerun the loaders on the current and/or parent routes, or maybe there was a change in search parameters. This can be achieved by specifying a \`to\` route path of \`"."\` which will reload the current location. This is only applicable to the current location, and hence any \`from\` route path specified is ignored.
3042
+ Quite often you might want to reload the current location or another \`from\` path, for example, to rerun the loaders on the current and/or parent routes, or maybe navigate back to a parent route. This can be achieved by specifying a \`to\` route path of \`"."\` which will reload the current location or provided \`from\` path.
3043
3043
 
3044
- Another common need is to navigate one route back relative to the current location or some other matched route in the current tree. By specifying a \`to\` route path of \`".."\` navigation will be resolved to either the first parent route preceding the current location or, if specified, preceding the \`"from"\` route path.
3044
+ Another common need is to navigate one route back relative to the current location or another path. By specifying a \`to\` route path of \`".."\` navigation will be resolved to the first parent route preceding the current location.
3045
3045
 
3046
3046
  \`\`\`tsx
3047
3047
  export const Route = createFileRoute('/posts/$postId')({
@@ -3052,7 +3052,14 @@ function PostComponent() {
3052
3052
  return (
3053
3053
  <div>
3054
3054
  <Link to=".">Reload the current route of /posts/$postId</Link>
3055
- <Link to="..">Navigate to /posts</Link>
3055
+ <Link to="..">Navigate back to /posts</Link>
3056
+ // the below are all equivalent
3057
+ <Link to="/posts">Navigate back to /posts</Link>
3058
+ <Link from="/posts" to=".">
3059
+ Navigate back to /posts
3060
+ </Link>
3061
+ // the below are all equivalent
3062
+ <Link to="/">Navigate to root</Link>
3056
3063
  <Link from="/posts" to="..">
3057
3064
  Navigate to root
3058
3065
  </Link>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-router",
3
- "version": "1.131.26",
3
+ "version": "1.131.28",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -79,8 +79,8 @@
79
79
  "isbot": "^5.1.22",
80
80
  "tiny-invariant": "^1.3.3",
81
81
  "tiny-warning": "^1.0.3",
82
- "@tanstack/router-core": "1.131.26",
83
- "@tanstack/history": "1.131.2"
82
+ "@tanstack/history": "1.131.2",
83
+ "@tanstack/router-core": "1.131.28"
84
84
  },
85
85
  "devDependencies": {
86
86
  "@testing-library/jest-dom": "^6.6.3",
package/src/index.tsx CHANGED
@@ -16,8 +16,6 @@ export {
16
16
  matchPathname,
17
17
  removeBasepath,
18
18
  matchByPath,
19
- encode,
20
- decode,
21
19
  rootRouteId,
22
20
  defaultSerializeError,
23
21
  defaultParseSearch,
package/src/link.tsx CHANGED
@@ -7,12 +7,12 @@ import {
7
7
  preloadWarning,
8
8
  removeTrailingSlash,
9
9
  } from '@tanstack/router-core'
10
+ import { useActiveLocation } from './useActiveLocation'
10
11
  import { useRouterState } from './useRouterState'
11
12
  import { useRouter } from './useRouter'
12
13
 
13
14
  import { useForwardedRef, useIntersectionObserver } from './utils'
14
15
 
15
- import { useMatch } from './useMatch'
16
16
  import type {
17
17
  AnyRouter,
18
18
  Constrain,
@@ -99,19 +99,27 @@ export function useLinkProps<
99
99
  structuralSharing: true as any,
100
100
  })
101
101
 
102
- const from = useMatch({
103
- strict: false,
104
- select: (match) => options.from ?? match.fullPath,
102
+ // subscribe to location here to re-build fromPath if it changes
103
+ const routerLocation = useRouterState({
104
+ select: (s) => s.location,
105
+ structuralSharing: true as any,
105
106
  })
106
107
 
107
- const next = React.useMemo(
108
- () => router.buildLocation({ ...options, from } as any),
108
+ const { getFromPath } = useActiveLocation()
109
+
110
+ const from = getFromPath(options.from)
111
+
112
+ const _options = React.useMemo(
113
+ () => {
114
+ return { ...options, from }
115
+ },
109
116
  // eslint-disable-next-line react-hooks/exhaustive-deps
110
117
  [
111
118
  router,
119
+ routerLocation,
112
120
  currentSearch,
113
- options._fromLocation,
114
121
  from,
122
+ options._fromLocation,
115
123
  options.hash,
116
124
  options.to,
117
125
  options.search,
@@ -122,6 +130,11 @@ export function useLinkProps<
122
130
  ],
123
131
  )
124
132
 
133
+ const next = React.useMemo(
134
+ () => router.buildLocation({ ..._options } as any),
135
+ [router, _options],
136
+ )
137
+
125
138
  const isExternal = type === 'external'
126
139
 
127
140
  const preload =
@@ -180,34 +193,12 @@ export function useLinkProps<
180
193
  },
181
194
  })
182
195
 
183
- const doPreload = React.useCallback(
184
- () => {
185
- router.preloadRoute({ ...options, from } as any).catch((err) => {
186
- console.warn(err)
187
- console.warn(preloadWarning)
188
- })
189
- },
190
- // eslint-disable-next-line react-hooks/exhaustive-deps
191
- [
192
- router,
193
- options.to,
194
- options._fromLocation,
195
- from,
196
- options.search,
197
- options.hash,
198
- options.params,
199
- options.state,
200
- options.mask,
201
- options.unsafeRelative,
202
- options.hashScrollIntoView,
203
- options.href,
204
- options.ignoreBlocker,
205
- options.reloadDocument,
206
- options.replace,
207
- options.resetScroll,
208
- options.viewTransition,
209
- ],
210
- )
196
+ const doPreload = React.useCallback(() => {
197
+ router.preloadRoute({ ..._options } as any).catch((err) => {
198
+ console.warn(err)
199
+ console.warn(preloadWarning)
200
+ })
201
+ }, [router, _options])
211
202
 
212
203
  const preloadViewportIoCallback = React.useCallback(
213
204
  (entry: IntersectionObserverEntry | undefined) => {
@@ -235,25 +226,6 @@ export function useLinkProps<
235
226
  }
236
227
  }, [disabled, doPreload, preload])
237
228
 
238
- if (isExternal) {
239
- return {
240
- ...propsSafeToSpread,
241
- ref: innerRef as React.ComponentPropsWithRef<'a'>['ref'],
242
- type,
243
- href: to,
244
- ...(children && { children }),
245
- ...(target && { target }),
246
- ...(disabled && { disabled }),
247
- ...(style && { style }),
248
- ...(className && { className }),
249
- ...(onClick && { onClick }),
250
- ...(onFocus && { onFocus }),
251
- ...(onMouseEnter && { onMouseEnter }),
252
- ...(onMouseLeave && { onMouseLeave }),
253
- ...(onTouchStart && { onTouchStart }),
254
- }
255
- }
256
-
257
229
  // The click handler
258
230
  const handleClick = (e: React.MouseEvent) => {
259
231
  if (
@@ -277,8 +249,7 @@ export function useLinkProps<
277
249
  // All is well? Navigate!
278
250
  // N.B. we don't call `router.commitLocation(next) here because we want to run `validateSearch` before committing
279
251
  router.navigate({
280
- ...options,
281
- from,
252
+ ..._options,
282
253
  replace,
283
254
  resetScroll,
284
255
  hashScrollIntoView,
@@ -289,6 +260,25 @@ export function useLinkProps<
289
260
  }
290
261
  }
291
262
 
263
+ if (isExternal) {
264
+ return {
265
+ ...propsSafeToSpread,
266
+ ref: innerRef as React.ComponentPropsWithRef<'a'>['ref'],
267
+ type,
268
+ href: to,
269
+ ...(children && { children }),
270
+ ...(target && { target }),
271
+ ...(disabled && { disabled }),
272
+ ...(style && { style }),
273
+ ...(className && { className }),
274
+ ...(onClick && { onClick }),
275
+ ...(onFocus && { onFocus }),
276
+ ...(onMouseEnter && { onMouseEnter }),
277
+ ...(onMouseLeave && { onMouseLeave }),
278
+ ...(onTouchStart && { onTouchStart }),
279
+ }
280
+ }
281
+
292
282
  // The click handler
293
283
  const handleFocus = (_: React.MouseEvent) => {
294
284
  if (disabled) return
@@ -0,0 +1,57 @@
1
+ import { last } from '@tanstack/router-core'
2
+ import { useCallback, useEffect, useState } from 'react'
3
+ import { useRouter } from './useRouter'
4
+ import { useMatch } from './useMatch'
5
+ import { useRouterState } from './useRouterState'
6
+ import type { ParsedLocation } from '@tanstack/router-core'
7
+
8
+ export type UseActiveLocationResult = {
9
+ activeLocation: ParsedLocation
10
+ getFromPath: (from?: string) => string
11
+ setActiveLocation: (location?: ParsedLocation) => void
12
+ }
13
+
14
+ export const useActiveLocation = (
15
+ location?: ParsedLocation,
16
+ ): UseActiveLocationResult => {
17
+ const router = useRouter()
18
+ const routerLocation = useRouterState({ select: (state) => state.location })
19
+ const [activeLocation, setActiveLocation] = useState<ParsedLocation>(
20
+ location ?? routerLocation,
21
+ )
22
+ const [customActiveLocation, setCustomActiveLocation] = useState<
23
+ ParsedLocation | undefined
24
+ >(location)
25
+
26
+ useEffect(() => {
27
+ setActiveLocation(customActiveLocation ?? routerLocation)
28
+ }, [routerLocation, customActiveLocation])
29
+
30
+ const matchIndex = useMatch({
31
+ strict: false,
32
+ select: (match) => match.index,
33
+ })
34
+
35
+ const getFromPath = useCallback(
36
+ (from?: string) => {
37
+ const activeLocationMatches = router.matchRoutes(activeLocation, {
38
+ _buildLocation: false,
39
+ })
40
+
41
+ const activeLocationMatch = last(activeLocationMatches)
42
+
43
+ return (
44
+ from ??
45
+ activeLocationMatch?.fullPath ??
46
+ router.state.matches[matchIndex]!.fullPath
47
+ )
48
+ },
49
+ [activeLocation, matchIndex, router],
50
+ )
51
+
52
+ return {
53
+ activeLocation,
54
+ getFromPath,
55
+ setActiveLocation: setCustomActiveLocation,
56
+ }
57
+ }
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react'
2
2
  import { useRouter } from './useRouter'
3
- import { useMatch } from './useMatch'
3
+ import { useActiveLocation } from './useActiveLocation'
4
4
  import type {
5
5
  AnyRouter,
6
6
  FromPathOption,
@@ -15,29 +15,21 @@ export function useNavigate<
15
15
  >(_defaultOpts?: {
16
16
  from?: FromPathOption<TRouter, TDefaultFrom>
17
17
  }): UseNavigateResult<TDefaultFrom> {
18
- const { navigate, state } = useRouter()
18
+ const router = useRouter()
19
19
 
20
- // Just get the index of the current match to avoid rerenders
21
- // as much as possible
22
- const matchIndex = useMatch({
23
- strict: false,
24
- select: (match) => match.index,
25
- })
20
+ const { getFromPath, activeLocation } = useActiveLocation()
26
21
 
27
22
  return React.useCallback(
28
23
  (options: NavigateOptions) => {
29
- const from =
30
- options.from ??
31
- _defaultOpts?.from ??
32
- state.matches[matchIndex]!.fullPath
24
+ const from = getFromPath(options.from ?? _defaultOpts?.from)
33
25
 
34
- return navigate({
26
+ return router.navigate({
35
27
  ...options,
36
28
  from,
37
29
  })
38
30
  },
39
31
  // eslint-disable-next-line react-hooks/exhaustive-deps
40
- [_defaultOpts?.from, navigate],
32
+ [_defaultOpts?.from, router, getFromPath, activeLocation],
41
33
  ) as UseNavigateResult<TDefaultFrom>
42
34
  }
43
35