Skip to content

Commit dca754e

Browse files
fix(react-router): fix custom link type ref inference (#4121)
Custom links created by `createLink` would incorrectly infer `ref` values for non-intrinsic components (such as those with their own `ref` prop like the [documented example](https://tanstack.com/router/latest/docs/framework/react/guide/custom-link#basic-example)), which would result in the ref being "double-wrapped". That is: Before: ```typescript { // Effective LinkComponentReactProps<TComp> // ... ref?: React.Ref<React.Ref<HTMLAnchorElement> | undefined> | undefined // notice the ref has been "ref'ed" twice } ``` After fix: ```typescript { // Effective LinkComponentReactProps<TComp> // ... ref?: React.Ref<HTMLAnchorElement> | undefined> } ``` This patch, the equivilant applied directly to `link.d.ts` via `yarn patch`, has been our workaround and it appears to work well: <details> <summary>@tanstack-react-router-npm-1.120.3-f6c72d7c75.patch</summary> ```patch diff --git a/dist/esm/link.d.ts b/dist/esm/link.d.ts index f7a4dfc2678f3669ee48b97c22e4c69d2f3ff195..be0adda3bf4302466c3c4b4d73ff6c78474a237f 100644 --- a/dist/esm/link.d.ts +++ b/dist/esm/link.d.ts @@ -4,9 +4,9 @@ import { ValidateLinkOptions, ValidateLinkOptionsArray } from './typePrimitives. import * as React from 'react'; export declare function useLinkProps<TRouter extends AnyRouter = RegisteredRouter, const TFrom extends string = string, const TTo extends string | undefined = undefined, const TMaskFrom extends string = TFrom, const TMaskTo extends string = ''>(options: UseLinkPropsOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>, forwardedRef?: React.ForwardedRef<Element>): React.ComponentPropsWithRef<'a'>; type UseLinkReactProps<TComp> = TComp extends keyof React.JSX.IntrinsicElements ? React.JSX.IntrinsicElements[TComp] : React.PropsWithoutRef<TComp extends React.ComponentType<infer TProps> ? TProps : never> & React.RefAttributes<TComp extends React.FC<{ - ref: infer TRef; + ref: React.Ref<infer TRef>; }> | React.Component<{ - ref: infer TRef; + ref: React.Ref<infer TRef>; }> ? TRef : never>; export type UseLinkPropsOptions<TRouter extends AnyRouter = RegisteredRouter, TFrom extends RoutePaths<TRouter['routeTree']> | string = string, TTo extends string | undefined = '.', TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom, TMaskTo extends string = '.'> = ActiveLinkOptions<'a', TRouter, TFrom, TTo, TMaskFrom, TMaskTo> & UseLinkReactProps<'a'>; export type ActiveLinkOptions<TComp = 'a', TRouter extends AnyRouter = RegisteredRouter, TFrom extends string = string, TTo extends string | undefined = '.', TMaskFrom extends string = TFrom, TMaskTo extends string = '.'> = LinkOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> & ActiveLinkOptionProps<TComp>; ``` </details> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent bdea54d commit dca754e

File tree

2 files changed

+37
-2
lines changed

2 files changed

+37
-2
lines changed

packages/react-router/src/link.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,8 +364,8 @@ type UseLinkReactProps<TComp> = TComp extends keyof React.JSX.IntrinsicElements
364364
> &
365365
React.RefAttributes<
366366
TComp extends
367-
| React.FC<{ ref: infer TRef }>
368-
| React.Component<{ ref: infer TRef }>
367+
| React.FC<{ ref: React.Ref<infer TRef> }>
368+
| React.Component<{ ref: React.Ref<infer TRef> }>
369369
? TRef
370370
: never
371371
>

packages/react-router/tests/link.test-d.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expectTypeOf, test } from 'vitest'
2+
import React from 'react'
23
import {
34
Link,
45
createLink,
@@ -9,6 +10,7 @@ import {
910
} from '../src'
1011
import type {
1112
CreateLinkProps,
13+
LinkComponent,
1214
ResolveRelativePath,
1315
SearchSchemaInput,
1416
} from '../src'
@@ -4005,6 +4007,39 @@ test('when passing a component with props to createLink and navigating to the ro
40054007
createLink((props) => expectTypeOf(props).toEqualTypeOf<CreateLinkProps>())
40064008
})
40074009

4010+
test('that createLink refs forward correctly', () => {
4011+
// copied from: https://tanstack.com/router/latest/docs/framework/react/guide/custom-link#basic-example
4012+
interface BasicLinkProps
4013+
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {}
4014+
const BasicLinkComponent = React.forwardRef<
4015+
HTMLAnchorElement,
4016+
BasicLinkProps
4017+
>((props, ref) => {
4018+
return (
4019+
<a ref={ref} {...props} className={'block px-3 py-2 text-blue-700'} />
4020+
)
4021+
})
4022+
const CreatedLinkComponent = createLink(BasicLinkComponent)
4023+
const CustomLink: LinkComponent<typeof BasicLinkComponent> = (props) => {
4024+
return <CreatedLinkComponent preload={'intent'} {...props} />
4025+
}
4026+
4027+
expectTypeOf(BasicLinkComponent)
4028+
.parameter(0)
4029+
.toHaveProperty('ref')
4030+
.toEqualTypeOf<React.Ref<HTMLAnchorElement> | undefined>()
4031+
4032+
expectTypeOf(CreatedLinkComponent)
4033+
.parameter(0)
4034+
.toHaveProperty('ref')
4035+
.toEqualTypeOf<Parameters<typeof BasicLinkComponent>[0]['ref']>()
4036+
4037+
expectTypeOf(CustomLink)
4038+
.parameter(0)
4039+
.toHaveProperty('ref')
4040+
.toEqualTypeOf<Parameters<typeof BasicLinkComponent>[0]['ref']>()
4041+
})
4042+
40084043
test('ResolveRelativePath', () => {
40094044
expectTypeOf<ResolveRelativePath<'/', '/posts'>>().toEqualTypeOf<'/posts'>()
40104045

0 commit comments

Comments
 (0)