Framework
Version
Integrationen

Typsicherheit

TanStack Router ist so konzipiert, dass er innerhalb der Grenzen des TypeScript-Compilers und der Laufzeit so typsicher wie möglich ist. Das bedeutet, dass er nicht nur in TypeScript geschrieben ist, sondern auch, dass er die von ihm bereitgestellten Typen vollständig ableitet und diese zäh durch die gesamte Routing-Erfahrung leitet.

Letztendlich bedeutet dies, dass Sie als Entwickler weniger Typen schreiben und mehr Vertrauen in Ihren Code haben, während er sich weiterentwickelt.

Routen-Definitionen

Datei-basierte Routenführung

Routen sind hierarchisch, ebenso wie ihre Definitionen. Wenn Sie datei-basierte Routenführung verwenden, ist bereits viel für die Typsicherheit gesorgt.

Code-basierte Routenführung

Wenn Sie die Route-Klasse direkt verwenden, müssen Sie darauf achten, wie Sie sicherstellen, dass Ihre Routen richtig typisiert werden, indem Sie die Option Route's getParentRoute verwenden. Das liegt daran, dass Kindrouten alle Typen ihrer Elternrouten kennen müssen. Ohne dies würden diese wertvollen Suchparameter, die Sie aus Ihren Layout- und pfadlosen Layout-Routen, 3 Ebenen darüber, extrahiert haben, im JS-Nirwana verloren gehen.

Vergessen Sie also nicht, die Elternroute an Ihre Kindrouten zu übergeben!

tsx
const parentRoute = createRoute({
  getParentRoute: () => parentRoute,
})
const parentRoute = createRoute({
  getParentRoute: () => parentRoute,
})

Exportierte Hooks, Komponenten und Dienstprogramme

Damit die Typen Ihres Routers mit Top-Level-Exporten wie Link, useNavigate, useParams usw. funktionieren, müssen sie die Modulgrenze von TypeScript durchdringen und direkt in die Bibliothek registriert werden. Dazu verwenden wir Deklarationsverschmelzung auf der exportierten Register-Schnittstelle.

ts
const router = createRouter({
  // ...
})

declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}
const router = createRouter({
  // ...
})

declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

Durch die Registrierung Ihres Routers im Modul können Sie nun die exportierten Hooks, Komponenten und Dienstprogramme mit den exakten Typen Ihres Routers verwenden.

Behebung des Komponenten-Kontext-Problems

Der Komponenten-Kontext ist ein hervorragendes Werkzeug in React und anderen Frameworks, um Abhängigkeiten für Komponenten bereitzustellen. Wenn sich jedoch die Typen dieses Kontexts bei der Weitergabe in Ihrer Komponenten-Hierarchie ändern, ist es für TypeScript unmöglich zu wissen, wie diese Änderungen abgeleitet werden sollen. Um dies zu umgehen, erfordern kontextbasierte Hooks und Komponenten, dass Sie ihnen einen Hinweis geben, wie und wo sie verwendet werden.

tsx
export const Route = createFileRoute('/posts')({
  component: PostsComponent,
})

function PostsComponent() {
  // Each route has type-safe versions of most of the built-in hooks from TanStack Router
  const params = Route.useParams()
  const search = Route.useSearch()

  // Some hooks require context from the *entire* router, not just the current route. To achieve type-safety here,
  // we must pass the `from` param to tell the hook our relative position in the route hierarchy.
  const navigate = useNavigate({ from: Route.fullPath })
  // ... etc
}
export const Route = createFileRoute('/posts')({
  component: PostsComponent,
})

function PostsComponent() {
  // Each route has type-safe versions of most of the built-in hooks from TanStack Router
  const params = Route.useParams()
  const search = Route.useSearch()

  // Some hooks require context from the *entire* router, not just the current route. To achieve type-safety here,
  // we must pass the `from` param to tell the hook our relative position in the route hierarchy.
  const navigate = useNavigate({ from: Route.fullPath })
  // ... etc
}

Jeder Hook und jede Komponente, die einen Kontext-Hinweis benötigt, verfügt über einen from-Parameter, dem Sie die ID oder den Pfad der Route übergeben können, innerhalb derer Sie rendern.

🧠 Schneller Tipp: Wenn Ihre Komponente Code-gesplittet ist, können Sie die getRouteApi Funktion verwenden, um zu vermeiden, dass Sie Route.fullPath übergeben müssen, um Zugriff auf die typisierten Hooks useParams() und useSearch() zu erhalten.

Was, wenn ich die Route nicht kenne? Was, wenn es eine gemeinsame Komponente ist?

Die Eigenschaft from ist optional, was bedeutet, dass Sie, wenn Sie sie nicht übergeben, die beste Vermutung des Routers erhalten, welche Typen verfügbar sein werden. Normalerweise erhalten Sie dann eine Union aller Typen aller Routen im Router.

Was, wenn ich den falschen from-Pfad angebe?

Es ist technisch möglich, einen from anzugeben, der TypeScript erfüllt, aber möglicherweise nicht der tatsächlichen Route entspricht, innerhalb derer Sie zur Laufzeit rendern. In diesem Fall erkennen jeder Hook und jede Komponente, die from unterstützt, ob Ihre Erwartungen nicht mit der tatsächlichen Route übereinstimmen, innerhalb derer Sie rendern, und lösen einen Laufzeitfehler aus.

Was, wenn ich die Route nicht kenne oder es eine gemeinsame Komponente ist und ich from nicht übergeben kann?

Wenn Sie eine Komponente rendern, die über mehrere Routen gemeinsam genutzt wird, oder eine Komponente rendern, die sich nicht innerhalb einer Route befindet, können Sie anstelle einer from-Option strict: false übergeben. Dies unterdrückt nicht nur den Laufzeitfehler, sondern liefert Ihnen auch lockere, aber genaue Typen für den potenziellen Hook, den Sie aufrufen. Ein gutes Beispiel hierfür ist das Aufrufen von useSearch aus einer gemeinsam genutzten Komponente.

tsx
function MyComponent() {
  const search = useSearch({ strict: false })
}
function MyComponent() {
  const search = useSearch({ strict: false })
}

In diesem Fall wird die Variable search als Union aller möglichen Suchparameter aus allen Routen im Router typisiert.

Router-Kontext

Der Router-Kontext ist äußerst nützlich, da er die ultimative hierarchische Abhängigkeitsinjektion darstellt. Sie können Kontext für den Router und für jede einzelne von ihm gerenderte Route bereitstellen. Wenn Sie diesen Kontext aufbauen, wird TanStack Router ihn mit der Hierarchie der Routen zusammenführen, sodass jede Route Zugriff auf den Kontext all ihrer Eltern hat.

Der Factory createRootRouteWithContext erstellt einen neuen Router mit dem instanziierten Typ, was dann eine Anforderung für Sie schafft, denselben Typvertrag für Ihren Router zu erfüllen, und stellt außerdem sicher, dass Ihr Kontext im gesamten Routenbaum korrekt typisiert ist.

tsx
const rootRoute = createRootRouteWithContext<{ whateverYouWant: true }>()({
  component: App,
})

const routeTree = rootRoute.addChildren([
  // ... all child routes will have access to `whateverYouWant` in their context
])

const router = createRouter({
  routeTree,
  context: {
    // This will be required to be passed now
    whateverYouWant: true,
  },
})
const rootRoute = createRootRouteWithContext<{ whateverYouWant: true }>()({
  component: App,
})

const routeTree = rootRoute.addChildren([
  // ... all child routes will have access to `whateverYouWant` in their context
])

const router = createRouter({
  routeTree,
  context: {
    // This will be required to be passed now
    whateverYouWant: true,
  },
})

Leistungs-Empfehlungen

Wenn Ihre Anwendung skaliert, werden die TypeScript-Prüfzeiten natürlich zunehmen. Es gibt einige Dinge, die Sie bei der Skalierung Ihrer Anwendung beachten sollten, um Ihre TS-Prüfzeiten niedrig zu halten.

Leiten Sie nur die Typen ab, die Sie benötigen

Ein großartiges Muster bei Client-seitigen Daten-Caches (TanStack Query usw.) ist das Vorab-Abrufen von Daten. Zum Beispiel könnten Sie mit TanStack Query eine Route haben, die queryClient.ensureQueryData in einem loader aufruft.

tsx
export const Route = createFileRoute('/posts/$postId/deep')({
  loader: ({ context: { queryClient }, params: { postId } }) =>
    queryClient.ensureQueryData(postQueryOptions(postId)),
  component: PostDeepComponent,
})

function PostDeepComponent() {
  const params = Route.useParams()
  const data = useSuspenseQuery(postQueryOptions(params.postId))

  return <></>
}
export const Route = createFileRoute('/posts/$postId/deep')({
  loader: ({ context: { queryClient }, params: { postId } }) =>
    queryClient.ensureQueryData(postQueryOptions(postId)),
  component: PostDeepComponent,
})

function PostDeepComponent() {
  const params = Route.useParams()
  const data = useSuspenseQuery(postQueryOptions(params.postId))

  return <></>
}

Dies mag in Ordnung aussehen und für kleine Routenbäume bemerken Sie möglicherweise keine TS-Leistungsprobleme. In diesem Fall muss TS jedoch den Rückgabetyp des Loaders ableiten, obwohl er in Ihrer Route nie verwendet wird. Wenn die Loader-Daten ein komplexer Typ sind und viele Routen auf diese Weise vorab abgerufen werden, kann dies die Editor-Leistung verlangsamen. In diesem Fall ist die Änderung ganz einfach und lässt TypeScript Promise ableiten..

tsx
export const Route = createFileRoute('/posts/$postId/deep')({
  loader: async ({ context: { queryClient }, params: { postId } }) => {
    await queryClient.ensureQueryData(postQueryOptions(postId))
  },
  component: PostDeepComponent,
})

function PostDeepComponent() {
  const params = Route.useParams()
  const data = useSuspenseQuery(postQueryOptions(params.postId))

  return <></>
}
export const Route = createFileRoute('/posts/$postId/deep')({
  loader: async ({ context: { queryClient }, params: { postId } }) => {
    await queryClient.ensureQueryData(postQueryOptions(postId))
  },
  component: PostDeepComponent,
})

function PostDeepComponent() {
  const params = Route.useParams()
  const data = useSuspenseQuery(postQueryOptions(params.postId))

  return <></>
}

Auf diese Weise werden die Loader-Daten nie abgeleitet und die Ableitung wird aus dem Routenbaum in den ersten Aufruf von useSuspenseQuery verschoben.

Grenzen Sie auf relevante Routen ein, so weit Sie können

Betrachten Sie die folgende Verwendung von Link

tsx
<Link to=".." search={{ page: 0 }} />
<Link to="." search={{ page: 0 }} />
<Link to=".." search={{ page: 0 }} />
<Link to="." search={{ page: 0 }} />

Diese Beispiele sind schlecht für die TS-Leistung. Das liegt daran, dass search zu einer Union aller search-Parameter für alle Routen aufgelöst wird und TS alles überprüfen muss, was Sie an die search-Prop übergeben, gegen diese potenziell große Union. Wenn Ihre Anwendung wächst, steigt diese Prüfzeit linear mit der Anzahl der Routen und Suchparameter. Wir haben unser Bestes getan, um diesen Fall zu optimieren (TypeScript wird diese Arbeit normalerweise einmal ausführen und zwischenspeichern), aber die anfängliche Prüfung gegen diese große Union ist teuer. Dies gilt auch für params und andere APIs wie useSearch, useParams, useNavigate usw.

Stattdessen sollten Sie versuchen, mit from oder to auf relevante Routen einzuschränken.

tsx
<Link from={Route.fullPath} to=".." search={{page: 0}} />
<Link from="/posts" to=".." search={{page: 0}} />
<Link from={Route.fullPath} to=".." search={{page: 0}} />
<Link from="/posts" to=".." search={{page: 0}} />

Denken Sie daran, dass Sie immer eine Union an to oder from übergeben können, um die Routen, an denen Sie interessiert sind, einzuschränken.

tsx
const from: '/posts/$postId/deep' | '/posts/' = '/posts/'
<Link from={from} to='..' />
const from: '/posts/$postId/deep' | '/posts/' = '/posts/'
<Link from={from} to='..' />

Sie können auch Zweige an from übergeben, um search oder params nur von Nachkommen dieses Zweigs zu beziehen.

tsx
const from = '/posts'
<Link from={from} to='..' />
const from = '/posts'
<Link from={from} to='..' />

/posts könnte ein Zweig mit vielen Nachkommen sein, die denselben search oder params haben.

Erwägen Sie die Verwendung der Objekt-Syntax von addChildren

Es ist üblich, dass Routen params, search, loaders oder context haben, die sogar auf externe Abhängigkeiten verweisen können, welche ebenfalls TS-Inferenzen stark beanspruchen. Für solche Anwendungen kann die Verwendung von Objekten zur Erstellung des Routenbaums performanter sein als Tupel.

createChildren kann auch ein Objekt akzeptieren. Für große Routenbäume mit komplexen Routen und externen Bibliotheken können Objekte für die Typenprüfung durch TS deutlich schneller sein als große Tupel. Die Leistungssteigerungen hängen von Ihrem Projekt, den externen Abhängigkeiten und der Art und Weise ab, wie die Typen für diese Bibliotheken geschrieben sind.

tsx
const routeTree = rootRoute.addChildren({
  postsRoute: postsRoute.addChildren({ postRoute, postsIndexRoute }),
  indexRoute,
})
const routeTree = rootRoute.addChildren({
  postsRoute: postsRoute.addChildren({ postRoute, postsIndexRoute }),
  indexRoute,
})

Beachten Sie, dass diese Syntax ausführlicher ist, aber eine bessere TS-Leistung aufweist. Bei dateibasierter Routenführung wird der Routenbaum für Sie generiert, sodass ein ausführlicher Routenbaum kein Problem darstellt.

Vermeiden Sie interne Typen ohne Einschränkung

Es ist üblich, dass Sie Typen wiederverwenden möchten. Zum Beispiel könnten Sie versucht sein, LinkProps wie folgt zu verwenden.

tsx
const props: LinkProps = {
  to: '/posts/',
}

return (
  <Link {...props}>
)
const props: LinkProps = {
  to: '/posts/',
}

return (
  <Link {...props}>
)

Dies ist SEHR schlecht für die TS-Leistung. Das Problem hier ist, dass LinkProps keine Typargumente hat und daher ein extrem großer Typ ist. Er enthält search, was eine Union aller search-Parameter ist, er enthält params, was eine Union aller params ist. Beim Zusammenführen dieses Objekts mit Link wird ein struktureller Vergleich dieses riesigen Typs durchgeführt.

Stattdessen können Sie as const satisfies verwenden, um einen präzisen Typ abzuleiten und nicht direkt LinkProps, um die riesige Prüfung zu vermeiden.

tsx
const props = {
  to: '/posts/',
} as const satisfies LinkProps

return (
  <Link {...props}>
)
const props = {
  to: '/posts/',
} as const satisfies LinkProps

return (
  <Link {...props}>
)

Da props nicht vom Typ LinkProps ist, ist diese Prüfung günstiger, da der Typ viel präziser ist. Sie können die Typenprüfung weiter verbessern, indem Sie LinkProps einschränken.

tsx
const props = {
  to: '/posts/',
} as const satisfies LinkProps<RegisteredRouter, string '/posts/'>

return (
  <Link {...props}>
)
const props = {
  to: '/posts/',
} as const satisfies LinkProps<RegisteredRouter, string '/posts/'>

return (
  <Link {...props}>
)

Dies ist noch schneller, da wir gegen den eingeschränkten LinkProps-Typ prüfen.

Sie können dies auch verwenden, um den Typ von LinkProps auf einen bestimmten Typ einzuschränken, der als Prop oder Parameter einer Funktion verwendet wird.

tsx
export const myLinkProps = [
  {
    to: '/posts',
  },
  {
    to: '/posts/$postId',
    params: { postId: 'postId' },
  },
] as const satisfies ReadonlyArray<LinkProps>

export type MyLinkProps = (typeof myLinkProps)[number]

const MyComponent = (props: { linkProps: MyLinkProps }) => {
  return <Link {...props.linkProps} />
}
export const myLinkProps = [
  {
    to: '/posts',
  },
  {
    to: '/posts/$postId',
    params: { postId: 'postId' },
  },
] as const satisfies ReadonlyArray<LinkProps>

export type MyLinkProps = (typeof myLinkProps)[number]

const MyComponent = (props: { linkProps: MyLinkProps }) => {
  return <Link {...props.linkProps} />
}

Dies ist schneller als die direkte Verwendung von LinkProps in einer Komponente, da MyLinkProps ein viel präziserer Typ ist.

Eine weitere Lösung ist, LinkProps nicht zu verwenden und die Steuerungsinversion zu nutzen, um eine auf eine bestimmte Route eingeschränkte Link-Komponente zu rendern. Render-Props sind eine gute Methode, die Steuerung an den Benutzer einer Komponente zu invertieren.

tsx
export interface MyComponentProps {
  readonly renderLink: () => React.ReactNode
}

const MyComponent = (props: MyComponentProps) => {
  return <div>{props.renderLink()}</div>
}

const Page = () => {
  return <MyComponent renderLink={() => <Link to="/absolute" />} />
}
export interface MyComponentProps {
  readonly renderLink: () => React.ReactNode
}

const MyComponent = (props: MyComponentProps) => {
  return <div>{props.renderLink()}</div>
}

const Page = () => {
  return <MyComponent renderLink={() => <Link to="/absolute" />} />
}

Dieses spezielle Beispiel ist sehr schnell, da wir die Kontrolle darüber, wohin wir navigieren, an den Benutzer der Komponente umgekehrt haben. Der Link ist auf die exakte Route beschränkt, zu der wir navigieren möchten.

Unsere Partner
Code Rabbit
Netlify
Neon
Clerk
Convex
Sentry
Bytes abonnieren

Ihre wöchentliche Dosis JavaScript-Nachrichten. Jeden Montag kostenlos an über 100.000 Entwickler geliefert.

Bytes

Kein Spam. Jederzeit kündbar.

Bytes abonnieren

Ihre wöchentliche Dosis JavaScript-Nachrichten. Jeden Montag kostenlos an über 100.000 Entwickler geliefert.

Bytes

Kein Spam. Jederzeit kündbar.