# How to Customize the Kaneo Frontend Using TanStack Router and React Query

> Customize Kaneo frontend with TanStack Router and React Query. Learn to create file-based routes and implement typed React Query hooks for dynamic UIs.

- Repository: [kaneo.app/kaneo](https://github.com/usekaneo/kaneo)
- Tags: how-to-guide
- Published: 2026-08-06

---

**You customize Kaneo's frontend by creating file-based routes under `apps/web/src/routes` and implementing typed React Query hooks under `apps/web/src/hooks/queries` or `mutations`, leveraging the TanStack Router Vite plugin for automatic route tree generation.**

The Kaneo project (`usekaneo/kaneo`) organizes its web interface as a pnpm monorepo where the frontend lives in `apps/web`. The stack combines **TanStack Router** for file-based routing and **React Query** for server state management, offering a fully typed, declarative approach to extending the UI. Customizing the frontend requires understanding how the Vite plugin auto-generates routes from the filesystem and how query hooks interact with the centralized Query Client.

## Understanding the Kaneo Frontend Architecture

Kaneo's frontend architecture separates routing concerns from data fetching through two distinct layers.

**TanStack Router Configuration**

The routing layer is configured in [`apps/web/vite.config.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/vite.config.ts) (lines 14-18), where the `tanstackRouter` plugin scans `apps/web/src/routes/**/*.tsx` to generate a typed route tree at build time. The runtime router component lives in [`apps/web/src/tanstack/router.tsx`](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/tanstack/router.tsx), which lazy-loads devtools in non-production environments (lines 4-10). Each route file exports a `Route` object created via `createFileRoute("/path")`, as seen in [`apps/web/src/routes/auth/sign-in.tsx`](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/routes/auth/sign-in.tsx).

**React Query Integration**

Data fetching is handled by hooks organized under `apps/web/src/hooks/queries/*` (for reads) and `apps/web/src/hooks/mutations/*` (for writes). These hooks wrap API fetchers and return standard `useQuery` or `useMutation` results with typed keys like `["invitations","pending"]`. The `InvitationsPage` component (lines 66-68) demonstrates cache invalidation patterns using `queryClient.invalidateQueries` after mutations complete.

## Customizing Routes with TanStack Router

Routes in Kaneo follow a filesystem-based convention where the URL path mirrors the file location under `src/routes`.

### Adding a New Page

Create a TypeScript file under `apps/web/src/routes` that matches your desired URL structure:

```tsx
// apps/web/src/routes/reports/$reportId.tsx
import { createFileRoute } from "@tanstack/react-router";
import { useReport } from "@/hooks/queries/report/use-get-report";

export const Route = createFileRoute("/reports/$reportId")({
  component: ReportPage,
});

function ReportPage() {
  const { reportId } = Route.useParams();
  const { data, isLoading } = useReport(reportId);
  
  if (isLoading) return <div>Loading...</div>;
  return <div>{data.title}</div>;
}

```

The `$reportId` syntax denotes a URL parameter. Navigate to this route programmatically using `useNavigate`:

```tsx
const navigate = useNavigate();
navigate({ to: "/reports/$reportId", params: { reportId: "abc123" } });

```

### Modifying Existing Routes

To override built-in behavior, edit the corresponding file in `src/routes`. For example, modifying [`src/routes/_layout/_authenticated/invitations.tsx`](https://github.com/usekaneo/kaneo/blob/main/src/routes/_layout/_authenticated/invitations.tsx) alters the invitations page without requiring additional registration steps—the Vite plugin detects changes automatically.

### Route-Level Code Splitting

TanStack Router automatically code-splits each route file. The `autoCodeSplitting` option is already enabled in [`vite.config.ts`](https://github.com/usekaneo/kaneo/blob/main/vite.config.ts) (lines 15-16), ensuring custom routes load on-demand without manual chunk configuration.

## Customizing Data Fetching with React Query

Kaneo uses React Query for server state management, with strict conventions for query keys and cache invalidation.

### Creating a Query Hook

Place new read hooks under `apps/web/src/hooks/queries/` following the existing pattern:

```tsx
// apps/web/src/hooks/queries/report/use-get-report.ts
import { useQuery } from "@tanstack/react-query";
import { getReport } from "@/fetchers/report/get-report";

export function useGetReport(reportId: string) {
  return useQuery({
    queryKey: ["report", reportId],
    queryFn: () => getReport(reportId),
    staleTime: 5 * 60_000, // 5 minutes
  });
}

```

This matches the pattern used in `usePendingInvitations` at [`apps/web/src/hooks/queries/invitation/use-pending-invitations.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/hooks/queries/invitation/use-pending-invitations.ts).

### Creating a Mutation Hook

Write mutations that automatically invalidate related queries:

```tsx
// apps/web/src/hooks/mutations/report/use-update-report.ts
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateReport } from "@/fetchers/report/update-report";

export function useUpdateReport() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: (payload) => updateReport(payload),
    onSuccess: (_, vars) => {
      // Refresh the specific report query
      queryClient.invalidateQueries({ queryKey: ["report", vars.id] });
    },
  });
}

```

### Cache Invalidation Patterns

After modifying server state, trigger refetches by invalidating specific query keys. The `InvitationsPage` component demonstrates this by calling `queryClient.invalidateQueries` (lines 66-68) to refresh pending invitations after accepting an invite.

## Complete Customization Example

Below is a complete workflow adding a Reports section with navigation, data fetching, and mutations.

**1. Create the Route List Page**

```tsx
// apps/web/src/routes/reports/index.tsx
import { createFileRoute, Link } from "@tanstack/react-router";

export const Route = createFileRoute("/reports")({
  component: ReportsList,
});

function ReportsList() {
  const { data: reports = [], isLoading } = useGetReports();

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <h1>My Reports</h1>
      <ul>
        {reports.map((r) => (
          <li key={r.id}>
            <Link to="/reports/$reportId" params={{ reportId: r.id }}>
              {r.title}
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

```

**2. Consume Data in Components**

```tsx
import { useGetReport } from "@/hooks/queries/report/use-get-report";

function ReportDetail({ reportId }: { reportId: string }) {
  const { data, isLoading, error } = useGetReport(reportId);

  if (isLoading) return <Spinner />;
  if (error) return <ErrorBox message={error.message} />;

  return (
    <section>
      <h2>{data.title}</h2>
      <p>{data.content}</p>
    </section>
  );
}

```

**3. Handle Mutations with Navigation**

```tsx
import { useUpdateReport } from "@/hooks/mutations/report/use-update-report";
import { useNavigate } from "@tanstack/react-router";

function EditReport({ report }: { report: Report }) {
  const navigate = useNavigate();
  const { mutateAsync, isPending } = useUpdateReport();

  const onSave = async (updates) => {
    await mutateAsync({ id: report.id, ...updates });
    navigate({ to: "/reports/$reportId", params: { reportId: report.id } });
  };

  return <ReportForm initial={report} onSubmit={onSave} disabled={isPending} />;
}

```

## Summary

- **File-based routing**: Create `.tsx` files under `apps/web/src/routes` to define new pages; the TanStack Router Vite plugin in [`vite.config.ts`](https://github.com/usekaneo/kaneo/blob/main/vite.config.ts) automatically generates the route tree.
- **Typed parameters**: Use `$paramName` syntax in filenames for dynamic segments, accessed via `Route.useParams()`.
- **Data hooks**: Place `useQuery` wrappers under `src/hooks/queries/` and `useMutation` wrappers under `src/hooks/mutations/` with stable query keys.
- **Cache management**: Call `queryClient.invalidateQueries` after mutations to refresh related data, following the pattern in [`apps/web/src/routes/_layout/_authenticated/invitations.tsx`](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/routes/_layout/_authenticated/invitations.tsx).
- **Development workflow**: Run `pnpm dev` from the monorepo root; Vite hot-reloads both route definitions and query hook changes.

## Frequently Asked Questions

### How does TanStack Router generate routes in Kaneo?

The TanStack Router Vite plugin configured in [`apps/web/vite.config.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/vite.config.ts) (lines 14-18) scans the `apps/web/src/routes/` directory at build time. It processes files like [`apps/web/src/routes/auth/sign-in.tsx`](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/routes/auth/sign-in.tsx) and automatically generates the route tree based on the filesystem hierarchy, eliminating manual route registration.

### Where should I place new React Query hooks in the Kaneo codebase?

Create query hooks under `apps/web/src/hooks/queries/` and mutation hooks under `apps/web/src/hooks/mutations/`. Follow the existing naming convention: `use-[resource]-[action].ts` (e.g., [`use-get-report.ts`](https://github.com/usekaneo/kaneo/blob/main/use-get-report.ts) or [`use-update-report.ts`](https://github.com/usekaneo/kaneo/blob/main/use-update-report.ts)). Each hook should import fetchers from `src/fetchers/` and export a function wrapping `useQuery` or `useMutation`.

### How do I invalidate cached data after a mutation?

Import `useQueryClient` from `@tanstack/react-query` inside your mutation hook. In the mutation's `onSuccess` callback, call `queryClient.invalidateQueries({ queryKey: ["yourKey", id] })` to mark specific queries as stale. This triggers automatic refetching in components using those query keys, as demonstrated in the invitations page at lines 66-68.

### Can I use code splitting for custom routes in Kaneo?

Yes. TanStack Router automatically code-splits every route file in the `src/routes/` directory. The `autoCodeSplitting` option is enabled by default in [`apps/web/vite.config.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/vite.config.ts) (lines 15-16), ensuring custom routes are bundled into separate chunks loaded only when the user navigates to that path.