React-Deep-Dive: TanStack Router und TanStack Query – Teil 1
TanStack Router and Query bring a breath of fresh air to routing and data fetching and are even suitable as an alternative to classic state management tools.

(Image: erzeugt mit KI durch iX)
- Nils Hartmann
The TanStack Router is an alternative to the React Router, the de facto standard for routing in React applications. The TanStack team released the first stable version in December 2023. The router brings a number of interesting concepts, such as end-to-end type safety, file-based routes and the ability to offload parts of the application state to the URL.
The TanStack Query (formerly React Query) library for data fetching is now available in version 5 and is already very popular. Its flexibility, a sophisticated caching mechanism and also a high level of type safety should be emphasized.
The two libraries can be used independently of each other, but also work very well together and in some cases can even replace the use of a global state management tool such as Jotai, Zustand or even Redux.
TanStack is a collection of open source libraries for the development of single-page applications (SPA). In addition to TanStack Router and TanStack Query, there are TanStack Table and TanStack Form, for example. Many of the libraries that function under the TanStack "brand" are offered for React, Angular, Vue and other SPA libraries. However, the TanStack Router only works with React. TanStack became popular with the Table and Query libraries, which were initially only available for React and were called React Table and React Query at the time. The initiator of the two libraries, Tanner Linsley, introduced the term TanStack to make it clear that the libraries are no longer exclusively available for React.
TanStack Router and TanStack Query in action
A React single-page application for managing tasks is used here to demonstrate the two libraries. The tasks can be displayed in a sortable list and in a single or detailed view. Tasks can also be pinned for quick access.
The application is certainly not complete and would probably be easier to build as a classic server-side web application with this range of functions. However, such an application would probably have more interactive features in real life that would justify its development as an SPA (for example, creating and editing tasks). The focus below is deliberately placed on the topics of navigation (routing) and working with server-side data. The source code of the sample application is available on GitHub.
The TanStack Router works file system-based, similar to server-side routers. The structure of the file system, i.e., the organization into folders and the naming of files, plays an important role. By convention, the router can deduce from this which URLs or paths the application has and what should happen when a path is called up in the browser. It should be noted that the file system only plays a role at development and build time (a plug-in for Vite is used for this). At runtime, the TanStack Router is a purely client-side router, just like the React Router.
Unless configured otherwise, the router interprets all files below the src/routes directory in the project as routes (see illustration). For better readability, the parent directory is omitted in the following path specifications.
(Image: Nils Hartmann)
In the first step, the example application should have two routes that represent an “About us” (route: "/about"
) and a “Privacy” page (route: "/privacy"
). To do this, two files with the name of the respective route must be created. The Vite plug-in recognizes these files and generates code to configure the route, which can be adapted as required. As a rule, the component
property specifies which React component the router should render when the route is called in the browser.
The listing shows the code of the (simplified) routes for /about and /privacy:
// src/routes/about.tsx
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/about")({
component: AboutRouteComponent,
});
function AboutRouteComponent() {
return <div>About us { /* ... */ }</div>;
}
// src/routes/privacy.tsx
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/privacy")({
component: PrivacyRouteComponent,
});
function PrivacyRouteComponent () {
return <div>Privacy { /* ... */ }</div>;
}
The router supports code splitting by default: The Vite plug-in stores the JavaScript code of individual routes in a separate bundle. The browser only loads this when the respective route is called (lazy loading). This makes it possible to significantly reduce the amount of JavaScript code that the browser has to initially load and execute when the application is started, and to considerably speed up the start of the application. Code splitting can be automatically activated for all routes via configuration. Alternatively, a route file with the file extension .lazy.tsx can activate code splitting for the route (e.g., about.lazy.tsx).
The /about route is displayed as follows:
(Image: Nils Hartmann)
Type-safe navigation
The link component of the router allows you to move from one route to the next in the application. Among other things, this component expects the to
property, which points to the target route (similar to the href
attribute on the HTML-a element). The following listing shows the code for the GlobalNavBar
component, which represents the navigation bar of the application. Two links are rendered here that point to the About and Privacy routes. The activeProps
property allows properties to be set that the router only applies when the link is active. An active link is a link that points to a route that is currently being rendered. This allows the current route to be highlighted in color in a navigation bar, for example.
export default function GlobalNavBar() {
return (
<nav>
<Link
className={"AppLink"}
activeProps={{
className: "AppLink__active",
}}
to={"/about"}
>
About
</Link>
<Link
to={"/privacy"}
className={"AppLink"}
activeProps={{
className: "AppLink__active",
}}
>
Privacy
</Link>
</nav>
);
}
A special feature of the router is its consistent type safety. The to
property recognizes all routes of the application and only accepts valid values, i.e., routes that actually exist. To enter the correct values, TypeScript can support code completion, for example, as shown in the illustration. The Vite plug-in generates the corresponding types automatically. This ensures that the types match the routes that actually exist in the project and do not diverge after refactoring, for example.
(Image: Nils Hartmann)
Querying server-side data with TanStack Query
The example application should receive two additional routes that represent a list of tasks or a single task. To illustrate how the application loads the data from the backend via a REST API, only the list display will be used initially. This should be called up via the path "/tasks"
(see illustration). To do this, create a new directory with the name of the route (tasks). It contains the file index.tsx, which contains the route configuration. Here too, the Vite plug-in generates the required boilerplate code and also extends the TypeScript types so that the GlobalNavBar
component can now also contain a link to the tasks route without triggering a compile error.
(Image: Nils Hartmann)
TanStack Query should read the data in the tasks component. To show the functionality of TanStack Query isolated from the router, it reads the data here directly in the component as an example. A later example will use the loader function of a route.
TanStack Query offers several ways to execute server requests and read data. The central object for this is the QueryClient
created when the application is started. It manages the cache and other global settings. It also provides a React-independent API for executing queries. However, the QueryClient is not usually used directly within React components. Instead, React-specific hook functions such as useQuery
and useSuspenseQuery
are used here. The useQuery
hook is shown here as an example. The following example shows the use of React's Suspense feature with TanStack Query.
Regardless of how developers want to execute a query, they have to make at least two settings, the query options. Firstly, they specify a function –, the query function –, which is responsible for loading the data. Secondly, a query key must define where the read data is to be stored in the cache. The listing shows a simplified use of useQuery
:
function TaskListRouteComponent() {
const result = useQuery({
queryKey: ["tasks", "list"],
queryFn(): Promise<Task[]> {
return fetch("http://api.tasks").then(res => res.json());
},
});
if (result.isPending) {
// Daten werden noch gelesen
return <PlaceholderTaskTable />
}
if (result.isSuccess) {
// Daten wurden erfolgreich gelesen
return <TaskTable tasks={result.data} />
}
// Fehler
return "Die Tasks konnten nicht gelesen werden";
}
The contract of the query function is extremely simple: it must return a promise that contains the read data. If the function cannot read the data, for example because there is a problem with the network or the requested endpoint is not working, it must throw an error. The query key is used to write the read data to the cache and read it out again later.
If the tasks route is called, the router renders the TaskListRouteComponent
and the query is executed. If there is no data in the cache yet, the useQuery
function returns an object in which the "isPending"
information is set to true
. The component then displays a placeholder. After successfully reading the data, the component is re-rendered – This corresponds to the typical React behavior when the state of a component is changed.
The object returned by useQuery
now contains, among other things, the information "isSuccess"
, which indicates that the data is available, which can then be retrieved using "data"
. Using this (and other) status information, a component can therefore react precisely to the individual phases of a request's life cycle and output suitable information on the interface.
TanStack Query stores the read data in its global cache. When rendering the component, the library checks whether the data is in the cache (using the query key). If it is already there, the data is returned from the cache. In the example application, this means that when switching between the task list and another route and back to the task list, the data comes directly from the cache. Even if several simultaneously displayed components query the data for the same query key, TanStack Query ensures that the query is not executed more than once and that all components receive the same status from the cache.
By default, the read data remains in the cache, but is immediately considered “stale”. This means that the component receives the data back from the cache when it is queried again, but at the same time the query function runs again in the background to update the data in the cache. The data can therefore be displayed rapidly when switching between routes. As soon as the query has read new data in the background, the display is updated automatically. How long the data remains in the cache, when it is considered obsolete and under what circumstances it is read again can be set very flexibly globally and per query to map almost all conceivable requirements.
(Image: WD Ashari/Shutterstock.com)
enterJS 2025 will take place on May 7 and 8 in Mannheim. The conference offers a comprehensive look at the JavaScript-supported enterprise world. The focus is not only on the programming languages JavaScript and TypeScript themselves, but also on frameworks and tools, accessibility, practical reports, UI/UX and security.
Highlights from the program:
- Modern React applications with TanStack
- 4 critical anti-patterns in React/TypeScript development
- React: Single-page or full-stack application (workshop, May 6)
Tickets are available at an early bird price in the online store.