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
.tsxfiles underapps/web/src/routesto define new pages; the TanStack Router Vite plugin invite.config.tsautomatically generates the route tree. - Typed parameters: Use
$paramNamesyntax in filenames for dynamic segments, accessed viaRoute.useParams(). - Data hooks: Place
useQuerywrappers undersrc/hooks/queries/anduseMutationwrappers undersrc/hooks/mutations/with stable query keys. - Cache management: Call
queryClient.invalidateQueriesafter mutations to refresh related data, following the pattern inapps/web/src/routes/_layout/_authenticated/invitations.tsx. - Development workflow: Run
pnpm devfrom 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →