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

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 (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, 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.

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:

// 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:

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 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 (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:

// 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.

Creating a Mutation Hook

Write mutations that automatically invalidate related queries:

// 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

// 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

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

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 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.
  • 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 (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 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 or 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 (lines 15-16), ensuring custom routes are bundled into separate chunks loaded only when the user navigates to that path.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →