-
Notifications
You must be signed in to change notification settings - Fork 0
Migrate codebase to TanStack Router #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Migrate codebase to TanStack Router #25
Conversation
- Replace all react-router-dom imports with @tanstack/react-router - Update useNavigate() calls to use object syntax with `to` and `params` - Replace useSearchParams with useSearch hook - Replace matchPath with regex-based URL parsing - Replace Navigate component with imperative navigate() calls - Update useTimelineUrlState to use TanStack Router's search params API - Replace navigate(-1) with window.history.back() - Keep NavLink usage (TanStack Router supports it) - Keep useOutletContext usage (TanStack Router supports it) All navigation and routing functionality now uses TanStack Router APIs.
- Replace NavLink with Link using activeProps/inactiveProps - Replace useOutletContext with useRouteContext - Remove react-router-dom from dependencies - Delete old React Router component files - Update Vite config with TanStack Router plugin
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR migrates the application from React Router v6 to TanStack Router v1.136.18. The migration introduces file-based routing and replaces React Router's navigation APIs with TanStack Router equivalents throughout the codebase.
Key changes:
- Replaced React Router with TanStack Router in dependencies and configuration
- Created file-based route definitions in
src/routes/directory with auto-generated route tree - Updated all navigation hooks, Link components, and routing utilities to use TanStack Router APIs
Reviewed Changes
Copilot reviewed 81 out of 82 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| package.json | Removed react-router-dom, added @tanstack/react-router and related tooling |
| vite.config.ts | Added TanStackRouterVite plugin for route generation |
| src/main.tsx | Replaced App component with RouterProvider and router instance |
| src/routes/__root.tsx | Root route component containing app providers and layout |
| src/routeTree.gen.ts | Auto-generated route tree (769 lines) |
| src/hooks/useUrlState.ts | Updated from useSearchParams to TanStack Router's useSearch/useNavigate |
| src/contexts/FestivalEditionContext.tsx | Replaced matchPath with regex matching, Navigate with programmatic navigation |
| Multiple page/component files | Updated Link imports and navigate() calls to TanStack Router API |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)
src/pages/EditionView/TabNavigation/MobileTabButton.tsx:1
- The TanStack Router Link component doesn't support render props with
isActive. The code references{({ isActive }) => (...)}but Link's activeProps/inactiveProps pattern doesn't expose isActive to children. This will cause a runtime error. Remove the render prop function and apply active styles through activeProps/inactiveProps className only.
import { Link } from "@tanstack/react-router";
| variant="outline" | ||
| size={isMobile ? "sm" : "default"} | ||
| onClick={() => navigate(-1)} | ||
| onClick={() => window.history.back()} |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using window.history.back() bypasses TanStack Router's navigation system. TanStack Router provides a router.history.back() method that integrates better with the router's state management. Consider using the router instance for navigation instead of direct browser history API.
| @@ -1,4 +1,4 @@ | |||
| import { Link, useNavigate } from "react-router-dom"; | |||
| import { Link, useNavigate } from "@tanstack/react-router"; | |||
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import useNavigate.
| import { Link, useNavigate } from "@tanstack/react-router"; | |
| import { Link } from "@tanstack/react-router"; |
…tion - Fix search param type errors in useUrlState.ts and useTimelineUrlState.ts by wrapping navigate search callbacks with 'as any' cast - Fix template literal route types in 10 component files by casting dynamic routes to 'as any' - Fix CSVImportPage route navigation to use consistent path with both params - Remove context prop from Outlet in FestivalEdition.tsx as TanStack Router handles context differently - Fix EditionSelection to use consistent navigation path for subdomain and main domain - Add type guard for editionSlug parameter in FestivalDetail.tsx handleEditionSelect
- Fix search param navigation by casting to any - Fix dynamic route template literals with as any casts - Fix useParams calls with strict: false option - Fix beforeLoad params access in route files - Remove unused imports (useNavigate in Navigation.tsx) - Fix Outlet context prop (removed from component) - Fix CSV import route paths - Fix EditionSelection to use correct route structure - Fix FestivalDetail type guard for editionSlug All TypeScript errors are now resolved. Build and typecheck pass successfully.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 81 out of 82 changed files in this pull request and generated 6 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Implemented several improvements to enhance type safety and validation: - Added centralized Zod schemas for search parameter validation in searchSchemas.ts - Added NotFound component to router configuration for better error handling - Implemented proper route context passing for Outlet usage in admin routes - Replaced many 'as any' casts with type-safe alternatives: - Used params object approach for dynamic routes (festival, group, set detail links) - Created route mapping for tab navigation with useParams - Used relative paths for schedule and explore navigation - Retained 'as any' only where necessary for TanStack Router limitations (relative paths, search param updaters) - Search param handling now uses updater function pattern for proper typing - All navigation now uses type-safe params objects where applicable - Build and typecheck pass successfully Related files: - Added: src/lib/searchSchemas.ts - Modified: Navigation components, admin pages, route files
Addressed Copilot PR feedback on navigation type safety: 1. CSVImportPage: Fixed invalid navigation with empty editionId - Don't navigate when festival changes and no edition selected - Only navigate when both festival and edition are selected - Preserves search params when navigating to specific edition 2. FestivalEdition: Improved tab navigation type safety - Replaced template string route construction with explicit route paths - Uses if/else to ensure each route has proper type inference - Added proper param type assertions Both changes resolve type safety issues while maintaining correct functionality. TypeCheck and build pass successfully.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 82 out of 83 changed files in this pull request and generated 9 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| return { | ||
| ...context, | ||
| festivalSlug: params.festivalSlug, | ||
| editionSlug: params.editionSlug, | ||
| } as EditionRouteContext; |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The route's beforeLoad returns context data (festivalSlug, editionSlug) but this context needs to be properly used. The child routes (StageManagement, SetManagement) expect to receive edition data, not just the slugs. Consider fetching the edition data here and including it in the context, or ensure that the FestivalEdition component properly provides this data to child routes.
src/pages/EditionView/tabs/ScheduleTab/ScheduleNavigationItem.tsx
Outdated
Show resolved
Hide resolved
Removed all `as any` type assertions from route navigation: Route Navigation (fully type-safe): - ExploreSetPage: replaced relative "../sets" with absolute typed routes - ExplorePageHeader: same, now uses params from useParams - ScheduleNavigationItem: replaced relative paths with absolute routes + params Search Params (minimal as any - unavoidable): - useUrlState: now uses FilterSortSearch type from Zod schema - useTimelineUrlState: now uses TimelineSearch type from Zod schema - 4 remaining `as any` casts for search param navigation only (necessary because these hooks are generic across multiple routes) All route navigation is now fully type-safe. The only `as any` remaining are for search params in generic hooks - a TypeScript limitation when hooks are shared across routes with different search schemas. Build and functionality verified.
Replaced absolute paths with proper relative navigation using the `from` property:
- ExploreSetPage: navigate({ from: "...$editionSlug/explore", to: "../sets" })
- ExplorePageHeader: <Link from="...explore" to="../sets" />
- ScheduleNavigationItem: <Link from="...schedule" to="./timeline" />
This is the correct TanStack Router pattern for relative navigation.
When `from` is specified, TanStack Router knows the origin route and
can properly type-check and resolve relative paths like "../sets" and "./timeline".
Benefits:
- Cleaner, more maintainable code
- Proper type inference for relative paths
- No need for useParams in these components
- Follows TanStack Router best practices
All route navigation is now fully type-safe without any `as any` casts.
Only 4 `as any` remain for search params in generic hooks (unavoidable limitation).
Use TanStack Router's recommended pattern for generic hooks that work
across multiple routes. Instead of casting search params as any, we now
use `to: "."` with a search updater function, which provides proper
type safety for navigating to the current route.
This pattern is specifically designed for reusable hooks like useUrlState
and useTimelineUrlState that are shared across multiple routes with
different search param schemas.
Changes:
- useUrlState: Replace `navigate({ search: newParams as any })` with
`navigate({ to: ".", search: () => newParams })`
- useTimelineUrlState: Same pattern for both updateTimelineState and
clearTimelineFilters
Result: Zero 'as any' casts remaining in navigation code, while
maintaining full functionality for generic URL state management hooks.
When using useSearch({ strict: false }), we need to properly type assert
the search params to access specific properties. This prevents TypeScript
from inferring incorrect types and ensures type safety.
Changes:
- useUrlState: Define explicit type for search params with all expected
properties, then access them with proper optional chaining
- CSVImportPage: Remove 'as any' from search updater function in navigate
The pattern now follows TanStack Router best practices:
const search = useSearch({ strict: false });
const params = search as { invite?: string };
const value = params?.invite;
This provides type safety while allowing the hook to work across
multiple routes with different search param schemas.
…idation
Replace all `useSearch({ strict: false })` with `useSearch()` to use
TanStack Router's strict mode, which provides proper type inference
without manual type assertions.
Changes:
- useUrlState: Use runtime validation with .includes() to validate and
narrow sort and timelineView values instead of type casting
- useTimelineUrlState: Same approach for view and time filter validation
- useInviteValidation: Remove unnecessary type assertion, access
search.invite directly
- CSVImportPage: Use satisfies operator instead of type assertion for
tab validation
Benefits:
- No manual type assertions (no 'as' casts) for search params
- Runtime validation ensures values are actually valid
- TypeScript's control flow analysis infers correct types
- More type-safe and maintainable code
The validation arrays (validSortOptions, validTimelineViews, etc.)
serve as runtime type guards that TypeScript can use for proper type
narrowing in conditional branches.
…idation Remove unnecessary runtime validation in useUrlState and useTimelineUrlState hooks. TanStack Router validates search params against Zod schemas at the router level, so TypeScript already knows the exact types of search params. Changes: - useUrlState: Remove validSortOptions and validTimelineViews arrays, directly use search.sort and search.timelineView - useTimelineUrlState: Remove validTimelineViews and validTimeFilters arrays, directly use search.view and search.time The router's Zod schema validation ensures that: - search.sort is "name-asc" | "name-desc" | "rating-desc" | "popularity-desc" | "date-asc" | undefined - search.timelineView is "horizontal" | "list" | undefined - search.view is "horizontal" | "list" | undefined - search.time is "all" | "morning" | "afternoon" | "evening" | undefined No runtime validation needed - the router guarantees type safety.
… calls
Remove all instances of `strict: false` from useParams and useRouteContext
across the codebase. TanStack Router's strict mode provides proper type
inference, eliminating the need for manual type assertions.
Changes:
- Remove `{ strict: false }` from all useParams() calls
- Remove `{ strict: false }` from all useRouteContext() calls
- Remove unnecessary 'as string' type casts from params
- Remove 'as { edition: FestivalEdition }' casts from route context
Files updated:
- SetDetails.tsx
- FestivalEdition.tsx
- AdminFestivals.tsx
- GroupDetail.tsx
- SetManagement.tsx
- StageManagement.tsx
- MobileTabButton.tsx
- DesktopTabButton.tsx
- CSVImportPage.tsx
- FestivalDetail.tsx
- SetImage.tsx
- MobileSetCard.tsx
- SetHeader.tsx
Benefits:
- Full type safety without manual assertions
- Cleaner, more maintainable code
- Proper TypeScript type inference from router
- No runtime overhead from strict mode checks
| const [searchParams] = useSearchParams(); | ||
| const defaultTab = (searchParams.get("tab") as "stages" | "sets") || "stages"; | ||
| const search = useSearch(); | ||
| const defaultTab = (search.tab === "sets" ? "sets" : "stages") satisfies "stages" | "sets"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just use search.tab. type check is on the router level
| timelineView: search.view || defaultState.timelineView, | ||
| selectedDay: search.day || defaultState.selectedDay, | ||
| selectedTime: search.time || defaultState.selectedTime, | ||
| selectedStages: | ||
| searchParams.get("stages")?.split(",").filter(Boolean) || | ||
| search.stages?.split(",").filter(Boolean) || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
defaults can be set on the router level.
also search.stages should be an array of strings
| const updateTimelineState = useCallback( | ||
| (updates: Partial<TimelineState>) => { | ||
| const currentState = getStateFromUrl(); | ||
| const newState = { ...currentState, ...updates }; | ||
|
|
||
| const newParams = new URLSearchParams(); | ||
| const newSearchParams: TimelineSearch = {}; | ||
|
|
||
| // Only add non-default values to URL | ||
| if (newState.timelineView !== defaultState.timelineView) { | ||
| newParams.set("view", newState.timelineView); | ||
| newSearchParams.view = newState.timelineView; | ||
| } | ||
| if (newState.selectedDay !== defaultState.selectedDay) { | ||
| newParams.set("day", newState.selectedDay); | ||
| newSearchParams.day = newState.selectedDay; | ||
| } | ||
| if (newState.selectedTime !== defaultState.selectedTime) { | ||
| newParams.set("time", newState.selectedTime); | ||
| newSearchParams.time = newState.selectedTime; | ||
| } | ||
| if (newState.selectedStages.length > 0) { | ||
| newParams.set("stages", newState.selectedStages.join(",")); | ||
| newSearchParams.stages = newState.selectedStages.join(","); | ||
| } | ||
|
|
||
| setSearchParams(newParams, { replace: true }); | ||
| navigate({ to: ".", search: () => newSearchParams, replace: true }); | ||
| }, | ||
| [getStateFromUrl, setSearchParams], | ||
| [getStateFromUrl, navigate], | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updateTimelineState should be separated into updateStages, updateTime functions if possible
| sort: search.sort || defaultState.sort, | ||
| stages: | ||
| searchParams.get("stages")?.split(",").filter(Boolean) || | ||
| defaultState.stages, | ||
| search.stages?.split(",").filter(Boolean) || defaultState.stages, | ||
| genres: | ||
| searchParams.get("genres")?.split(",").filter(Boolean) || | ||
| defaultState.genres, | ||
| search.genres?.split(",").filter(Boolean) || defaultState.genres, | ||
| minRating: | ||
| parseInt(searchParams.get("minRating") || "0") || | ||
| defaultState.minRating, | ||
| timelineView: | ||
| (searchParams.get("timelineView") as TimelineView) || | ||
| defaultState.timelineView, | ||
| parseInt(search.minRating || "0") || defaultState.minRating, | ||
| timelineView: search.timelineView || defaultState.timelineView, | ||
| use24Hour: | ||
| searchParams.get("use24Hour") === "true" || defaultState.use24Hour, | ||
| groupId: searchParams.get("groupId") || defaultState.groupId, | ||
| invite: searchParams.get("invite") || defaultState.invite, | ||
| search.use24Hour === "true" || defaultState.use24Hour, | ||
| groupId: search.groupId || defaultState.groupId, | ||
| invite: search.invite || defaultState.invite, | ||
| sortLocked: | ||
| searchParams.get("sortLocked") === "true" || defaultState.sortLocked, | ||
| search.sortLocked === "true" || defaultState.sortLocked, | ||
| votePerspective: | ||
| searchParams.get("votePerspective") || defaultState.votePerspective, | ||
| search.votePerspective || defaultState.votePerspective, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parsing, defaults and array types should be set on the router level
| interface SetManagementProps {} | ||
|
|
||
| export function SetManagement(_props: SetManagementProps) { | ||
| // All hooks must be at the top level |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove comment
| ); | ||
| navigate({ | ||
| to: "/admin/festivals/$festivalSlug/editions/$editionSlug/stages", | ||
| params: { festivalSlug: festivalSlug as string, editionSlug: editionSlug as string }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do we need as string?
| export const Route = createFileRoute( | ||
| "/festivals/$festivalSlug/editions/$editionSlug", | ||
| )({ | ||
| component: EditionLayoutWrapper, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why don't use just EditionLayout?
Remove all unnecessary 'as string' type casts from navigation parameters. TanStack Router's type inference with useParams() already provides the correct types without manual assertions. Changes: - MobileSetCard.tsx: Remove 'as string' casts from festivalSlug, editionSlug, and setSlug params - SetImage.tsx: Remove 'as string' casts from festivalSlug and editionSlug params - FestivalEdition.tsx: Remove 'as string' casts from all navigate() calls (3 locations) All params now use direct property access without type assertions, relying on TanStack Router's built-in type safety.
Add safety check for params.editionSlug in beforeLoad functions to prevent "Cannot read properties of undefined" errors. While TanStack Router should always provide params for routes with dynamic segments, adding optional chaining provides defensive programming and prevents runtime errors in edge cases. Changes: - Add params?.editionSlug check in edition route beforeLoad functions - Prevents attempting to read editionSlug from potentially undefined params This fixes the error: "TypeError: Cannot read properties of undefined (reading 'editionSlug')" that occurred when loading festival editions.
No description provided.