feat: migrate from React Router to Next.js App Router with proper RSC routes#1734
Closed
KevinWu098 wants to merge 8 commits into
Closed
feat: migrate from React Router to Next.js App Router with proper RSC routes#1734KevinWu098 wants to merge 8 commits into
KevinWu098 wants to merge 8 commits into
Conversation
… routes - Remove react-router-dom dependency entirely - Replace catch-all [[...slug]] with proper route segments: - (home)/ group for /, /added, /map with shared layout - /auth and /auth/native as separate pages - /unsubscribe/[userId] as a dynamic route - /feedback handled via next.config.mjs redirect - Create HomeShell client component (replaces App.tsx + Home.tsx) - Layout persists across tab navigation (no state loss) - Uses usePathname() for tab state sync instead of React Router useParams - Create providers.tsx for root-level context providers - Add per-route metadata exports for proper SEO - Add per-route server-rendered SEO content (sr-only) - Add error.tsx and not-found.tsx for proper error handling - Replace all react-router-dom Link with next/link or <a> for external URLs - Replace useNavigate with useRouter from next/navigation - Replace useSearchParams/useParams with next/navigation equivalents - Update sitemap.ts to include /added and /map routes - Use title template in root layout metadata
- Add Next.js middleware with OUTAGE flag for maintenance mode - Move OutagePage to proper /outage route under app/ - Remove legacy routes/ directory (all routes now in app/) - Remove old ErrorPage.tsx (replaced by app/error.tsx)
Next.js 16 uses proxy.ts instead of middleware.ts. Integrate the outage mode redirect into the existing proxy.ts CORS handler. Toggle OUTAGE constant to enable maintenance mode.
The routes/ directory has been removed; all routes now live under app/.
PostHog's constructor accesses `self` which is unavailable during
Next.js static page generation. Previously this was hidden behind
`dynamic(import, { ssr: false })`.
- Lazily initialize PostHog instance on first client-side access
- Export a no-op Proxy during SSR so direct imports don't crash
- Guard PostHogProvider rendering with `typeof window` check
The Zustand store initializer for useThemeStore accessed window.localStorage and window.matchMedia during module evaluation. Add typeof window checks to provide safe defaults during SSR/static generation.
Comprehensive fix for SSR/static generation compatibility. All Zustand store initializers and module-level code that accessed window, localStorage, or matchMedia now check typeof window !== 'undefined' first, providing safe defaults during server-side rendering. Fixed modules: - SessionStore.ts: window.localStorage.removeItem in initializer - ColumnStore.ts: getLocalStorageColumnToggles() at module level - CoursePaneStore.ts: window.location.search in paramsAreInURL() - PatchNotesStore.ts: shouldShowPatchNotes() calling localStorage/matchMedia - RightPaneStore.ts: window.location.search in constructor - TutorialHelpers.tsx: window.matchMedia in tourShouldRun() - plannerHelpers.ts: window.location.search in shouldSearchPlannerFromParams()
- Wrap PostHogPageviewTracker in <Suspense> (uses useSearchParams) - Mark all (home) pages as force-dynamic since the interactive app shell uses useSearchParams in multiple components and cannot be meaningfully statically generated - Mark auth and unsubscribe pages as force-dynamic (depend on query params) Next.js requires useSearchParams() to be inside a Suspense boundary during static generation. Since the home pages are fundamentally interactive SPA content, dynamic rendering is the correct choice.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Complete migration from the React Router "SSR shell around SPA" pattern to idiomatic Next.js App Router with proper route segments and React Server Components.
Architecture Changes
Before: Single catch-all
[[...slug]]/page.tsx→dynamic(() => import('../../App'), { ssr: false })→ React RoutercreateBrowserRouterhandling all navigation client-side.After: Proper Next.js App Router segments with RSC pages, shared layouts, and
next/navigationfor all routing.Route Structure
/[[...slug]]→ React Router/→ Homeapp/(home)/page.tsxRSC +HomeShellclient layout/added[[...slug]]→ React Router/:tab→ Homeapp/(home)/added/page.tsxRSC + shared layout/map[[...slug]]→ React Router/:tab→ Homeapp/(home)/map/page.tsxRSC + shared layout/feedbackwindow.location.replace()next.config.mjsredirect (server-side, no JS)/auth[[...slug]]→ React Router → AuthPageapp/auth/page.tsxwith client component/auth/nativeapp/auth/native/page.tsx/unsubscribe/:userId[[...slug]]→ React Routerapp/unsubscribe/[userId]/page.tsxwith RSC params/outageapp/outage/page.tsxwith proxy.ts guard/*(unknown)Navigateto/not-found.tsxKey Improvements
react-router-domcompletely removed — zero client router overheadMetadatafor proper SEOsr-onlycontent per route instead of one bloberror.tsxwithreset()capability replaceserrorElement/feedbackredirect handled at edge (no client JS)(home)group doesn't remount the app<a>— no moreLinkcomponent for external URLs (RateMyProfessors, bug reports)%s | AntAlmanacpattern/addedand/maproutesProvider Architecture
Tab System
The tab system now uses
usePathname()fromnext/navigationinstead ofuseParams()from React Router. Tab clicks usenext/linkfor navigation, and theScheduleManagementcomponent derives the active tab from the URL pathname. Since theHomeShelllives in the layout, it persists across tab navigation — no state loss.Files Changed
App.tsx,App.css,routes/directory,[[...slug]]/directoryapp/(home)/,app/auth/,app/unsubscribe/,app/outage/,app/error.tsx,app/not-found.tsx,app/providers.tsxreact-router-dom(14 files)Test Plan
oxlint --deny-warningspasses across all 244 source filesnext build)calendarize-helpers,parse-days-string,download-ics,patch-notes,schedule) requirepnpm get-datato run — this is pre-existing and unrelated to this PRIssues
Future Followup
createTRPCProxyClientcan be upgraded to@trpc/react-queryfor hook-based data fetching with caching, now that the React Query provider is properly positioned