Skip to content

refactor: migrate codebase to TanStack Router#25

Merged
chiptus merged 43 commits intomainfrom
claude/migrate-tanstack-router-015h5PFMhsh3FgDeb2uEpKgX
Apr 14, 2026
Merged

refactor: migrate codebase to TanStack Router#25
chiptus merged 43 commits intomainfrom
claude/migrate-tanstack-router-015h5PFMhsh3FgDeb2uEpKgX

Conversation

@chiptus
Copy link
Copy Markdown
Owner

@chiptus chiptus commented Nov 20, 2025

No description provided.

@vercel
Copy link
Copy Markdown

vercel Bot commented Nov 20, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
upline Ready Ready Preview, Comment Apr 14, 2026 6:34pm

Copy link
Copy Markdown

Copilot AI left a 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";

Comment thread src/pages/admin/festivals/FestivalEdition.tsx Outdated
Comment thread src/pages/ExploreSetPage/ExploreSetPage.tsx Outdated
Comment thread src/pages/ExploreSetPage/ExploreSetPage.tsx Outdated
Comment thread src/hooks/useUrlState.ts Outdated
Comment thread src/components/layout/AppHeader/Navigation.tsx Outdated
Comment thread src/pages/EditionSelection.tsx Outdated
Comment thread src/components/layout/AppHeader/Navigation.tsx Outdated
Comment thread src/routes/festivals/$festivalSlug/editions/$editionSlug.tsx Outdated
Copy link
Copy Markdown

Copilot AI left a 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

Comment thread src/pages/admin/festivals/FestivalEdition.tsx
Comment thread src/pages/groups/Groups/GroupCard.tsx Outdated
Comment thread src/pages/groups/GroupDetail.tsx Outdated
Comment thread src/pages/admin/festivals/CSVImportPage.tsx Outdated
Comment thread src/pages/admin/festivals/FestivalEdition.tsx Outdated
Copy link
Copy Markdown

Copilot AI left a 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

Comment thread src/hooks/useTimelineUrlState.ts Outdated
Comment thread src/hooks/useTimelineUrlState.ts Outdated
Comment thread src/pages/admin/festivals/FestivalEdition.tsx
Comment thread src/pages/ExploreSetPage/components/ExplorePageHeader.tsx Outdated
Comment thread src/pages/ExploreSetPage/ExploreSetPage.tsx Outdated
Comment thread src/pages/ExploreSetPage/ExploreSetPage.tsx Outdated
Comment thread src/hooks/useUrlState.ts Outdated
Comment thread src/routes/admin/festivals/$festivalSlug/editions/$editionSlug.tsx Outdated
Comment thread src/pages/EditionView/tabs/ScheduleTab/ScheduleNavigationItem.tsx Outdated
Comment thread src/pages/admin/festivals/SetsManagement/SetManagement.tsx Outdated
Comment thread src/pages/admin/festivals/AdminFestivals.tsx
Comment thread src/pages/admin/festivals/CSVImportPage.tsx Outdated
Comment thread src/pages/admin/festivals/CSVImportPage.tsx Outdated
Comment thread src/hooks/useTimelineUrlState.ts Outdated
Comment thread src/hooks/useTimelineUrlState.ts Outdated
Comment thread src/hooks/useUrlState.ts Outdated
Comment thread src/pages/admin/festivals/SetsManagement/SetManagement.tsx Outdated
Comment thread src/pages/admin/festivals/FestivalEdition.tsx Outdated
Comment thread src/routes/festivals/$festivalSlug/editions/$editionSlug.tsx Outdated
claude added 6 commits March 31, 2026 16:39
- 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
…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.
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.
- Remove extra slash in input rewrite pathname concatenation
- Fix output destructuring to extract festivalSlug (not "festivals")
- Simplify to use user's approach directly in createRouter

https://claude.ai/code/session_015h5PFMhsh3FgDeb2uEpKgX
URL rewrite in main.tsx guarantees /festivals/slug internal paths for
both subdomain and path-based routing, so simple regex on useLocation
pathname is sufficient - no routeId pattern matching or subdomain detection needed.

https://claude.ai/code/session_015h5PFMhsh3FgDeb2uEpKgX
URL rewrite output handles the /festivals/slug → /editions/slug transformation,
so always use the full TanStack Router path; remove duplicated navigate branches.

https://claude.ai/code/session_015h5PFMhsh3FgDeb2uEpKgX
- Create festivals/$festivalSlug.tsx layout route that wraps all festival
  routes and provides FestivalEditionContext using route params directly
- FestivalEditionProvider now accepts festivalSlug/editionSlug as props
  instead of parsing the URL pathname
- useFestivalEdition() returns safe defaults instead of throwing when
  used outside a festival route (e.g. FestivalSelection header components)
- Remove FestivalEditionProvider from __root.tsx

https://claude.ai/code/session_015h5PFMhsh3FgDeb2uEpKgX
…ading

- Create festivals/$festivalSlug.tsx layout route with a loader that
  preloads the festival via queryClient.ensureQueryData before rendering
- FestivalEditionProvider now receives festival: Festival as a prop
  (already fetched, no loading state) instead of fetching it internally
- Edition is preloaded in the $editionSlug.tsx beforeLoad via ensureQueryData
- Drop isContextReady from context - loaders guarantee data is ready
- Remove FestivalEditionProvider from __root.tsx
- Regenerate routeTree.gen.ts to include new layout route

https://claude.ai/code/session_015h5PFMhsh3FgDeb2uEpKgX
- AppBranding: remove useFestivalEdition(), link to "/" (URL rewrite
  maps "/" to festival home on subdomains, selection on main domain)
- FestivalIndicator: remove useFestivalEdition(), use title prop for alt
- Drop basePath from FestivalEditionContext entirely
- TabNavigation: tab buttons already use typed useParams(), drop basePath prop
- SetNotFoundState, EmptyState: use useParams() + typed Link to sets route
- Remove QueryClient cast in loaders (context.queryClient is already typed
  via RouterContext declared in __root.tsx)

Fixes all-shard CI test failures caused by useFestivalEdition() throwing
when rendered outside the festival layout route.

https://claude.ai/code/session_015h5PFMhsh3FgDeb2uEpKgX
Copy link
Copy Markdown

Copilot AI left a 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 97 out of 98 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment thread src/pages/admin/festivals/CSVImportPage.tsx Outdated
Comment thread src/main.tsx
Comment thread src/hooks/useUrlState.ts
…ng, useUrlState type safety

- Add hostname guard in URL rewrite output to prevent localhost breakage
- Use strict: false params/search in CSVImportPage for multi-route support
- Parse search through Zod schema in useUrlState for type safety and defaults

https://claude.ai/code/session_015h5PFMhsh3FgDeb2uEpKgX
Type safety improvements:
- Bind useUrlState to sets route for validated search params
- Remove manual Zod parsing (route already validates)
- Remove context cast in admin edition route
- Remove useParams cast in festival layout

Code deduplication:
- Import fetchFestivalEditionBySlug instead of duplicating in admin route
- Import fetchFestivalBySlug instead of duplicating in edition query

All changes improve type safety and reduce code duplication while maintaining
identical runtime behavior. Typecheck passes.

https://claude.ai/code/session_015h5PFMhsh3FgDeb2uEpKgX
- CSVImportPage: Remove params and search casts (strict:false still needed for multi-route usage)
- SetHeader: Bind to parent route instead of strict:false with defaults
- MobileSetCard: Bind to parent route instead of strict:false with defaults

Keep strict:false in parent layouts (AdminFestivals, FestivalDetail, $festivalSlug)
where they legitimately need to read optional child route params for UI state.

All type casts now removed. TypeScript enforces correct types throughout.

https://claude.ai/code/session_015h5PFMhsh3FgDeb2uEpKgX
Copy link
Copy Markdown

Copilot AI left a 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 97 out of 98 changed files in this pull request and generated 4 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment thread src/lib/searchSchemas.ts
Comment on lines +17 to +22
minRating: z.number().catch(0),
timelineView: timelineViewSchema.catch("list"),
use24Hour: z.boolean().catch(true),
groupId: z.string().optional(),
invite: z.string().optional(),
sortLocked: z.boolean().catch(false),
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filterSortSearchSchema uses z.number()/z.boolean() for query-string values (e.g. minRating, use24Hour, sortLocked). TanStack Router's default search parsing yields strings, so these fields will fail validation on page load/deep links and fall back to the .catch(...) defaults. Use z.coerce.number() / z.coerce.boolean() (or a z.preprocess) so values round-trip correctly via the URL.

Suggested change
minRating: z.number().catch(0),
timelineView: timelineViewSchema.catch("list"),
use24Hour: z.boolean().catch(true),
groupId: z.string().optional(),
invite: z.string().optional(),
sortLocked: z.boolean().catch(false),
minRating: z.coerce.number().catch(0),
timelineView: timelineViewSchema.catch("list"),
use24Hour: z.coerce.boolean().catch(true),
groupId: z.string().optional(),
invite: z.string().optional(),
sortLocked: z.coerce.boolean().catch(false),

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +16
component: ScheduleTab,
validateSearch: filterSortSearchSchema,
beforeLoad: ({ params, location }) => {
if (location.pathname.endsWith("/schedule")) {
throw redirect({
to: "/festivals/$festivalSlug/editions/$editionSlug/schedule/timeline",
params,
search: location.search as Record<string, unknown>,
});
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This route validates search params with filterSortSearchSchema, but the schedule children (/schedule/timeline and /schedule/list) validate with timelineSearchSchema. When redirecting from /schedule to /schedule/timeline, location.search will only include keys accepted by this route’s validateSearch, so day/time/view can be dropped if someone lands on /schedule?.... Consider switching this route to timelineSearchSchema (or making the schema passthrough) so the redirect preserves timeline search params.

Copilot uses AI. Check for mistakes.
Comment on lines +104 to +111
function handleClick(e: React.MouseEvent) {
const isMain = isMainGetuplineDomain();

if (isMain) {
e.preventDefault();
const subdomainUrl = createFestivalSubdomainUrl(festival.slug);
window.location.href = subdomainUrl;
}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleClick uses the React.MouseEvent type, but this file doesn’t import the React namespace (only useEffect). With the modern JSX transform, React isn’t in scope automatically, so this will fail typechecking. Import the needed type (e.g. import type { MouseEvent } from 'react') or import React as a type namespace.

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +27
export function MobileTabButton({ config }: TabButtonProps) {
const { festivalSlug, editionSlug } = useParams({
from: "/festivals/$festivalSlug/editions/$editionSlug",
});
const matchRoute = useMatchRoute();
const isActive = !!matchRoute({ to: tabRoutes[config.key] });

return (
<NavLink
<Link
key={config.key}
to={`${basePath}/${config.key}`}
className={({ isActive }) => `
flex-1 flex flex-col items-center justify-center
to={tabRoutes[config.key]}
params={{ festivalSlug, editionSlug }}
className={`flex-1 flex flex-col items-center justify-center
py-2 px-1 transition-colors duration-200 min-h-16
${isActive ? "text-purple-400" : "text-gray-400 active:text-purple-300"}
`}
${isActive ? "text-purple-400" : "text-gray-400 active:text-purple-300"}`}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useMatchRoute({ to: ... }) is used with default (exact) matching to compute isActive. This changes behavior from the previous NavLink (which was active for nested routes) and will likely cause tabs like “Sets” to not appear active on nested routes (e.g. set details) and “Schedule” to not appear active on /schedule/timeline or /schedule/list. Consider using fuzzy/partial matching (e.g. fuzzy: true) or Link activeOptions so parent tabs stay active for descendant routes.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants