Back to Cherry Studio

Routing System Developer Guide

src/renderer/routes/README.md

2.0.06.4 KB
Original Source

Routing System Developer Guide

This project uses TanStack Router + Multi MemoryRouter architecture, where each Tab has its own independent router instance, enabling native KeepAlive behavior.

Quick Start

1. Adding a New Page

Create a file in the src/renderer/routes/ directory:

typescript
// routes/knowledge.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/knowledge')({
  component: KnowledgePage
})

function KnowledgePage() {
  return <div>Knowledge Page</div>
}

After running yarn dev, TanStack Router will automatically update routeTree.gen.ts.

2. Routes with Parameters

typescript
// routes/chat/$topicId.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/chat/$topicId')({
  component: ChatPage
})

function ChatPage() {
  const { topicId } = Route.useParams()
  return <div>Chat: {topicId}</div>
}

3. Nested Routes

text
routes/
├── settings.tsx        # /settings (layout)
├── settings/
│   ├── general.tsx     # /settings/general
│   └── provider.tsx    # /settings/provider
typescript
// routes/settings.tsx
import { createFileRoute, Outlet } from '@tanstack/react-router'

export const Route = createFileRoute('/settings')({
  component: SettingsLayout
})

function SettingsLayout() {
  return (
    <div className="flex">
      <aside>Settings Menu</aside>
      <main><Outlet /></main>
    </div>
  )
}

This project provides two navigation methods:

1. Tab-Level Navigation - openTab

Open a new Tab or switch to an existing Tab using the useTabs hook:

typescript
import { useTabs } from '@renderer/hooks/tab'

function MyComponent() {
  const { openTab, closeTab } = useTabs()

  // Basic usage - reuse existing Tab or create new one
  openTab('/settings')

  // With title
  openTab('/chat/123', { title: 'Chat with Alice' })

  // Force new Tab (even if same URL exists)
  openTab('/settings', { forceNew: true })

  // Open Webview Tab
  openTab('https://example.com', {
    type: 'webview',
    title: 'Example Site'
  })

  // Close Tab
  closeTab(tabId)
}

2. In-Tab Navigation - useNavigate

Navigate within the same Tab (won't create a new Tab) using TanStack Router's useNavigate:

typescript
import { useNavigate } from '@tanstack/react-router'

function SettingsPage() {
  const navigate = useNavigate()

  // Navigate to sub-page within current Tab
  navigate({ to: '/settings/provider' })

  // Navigate with parameters
  navigate({ to: '/chat/$topicId', params: { topicId: '123' } })
}

Comparison

ScenarioMethodResult
Open new feature moduleopenTab('/knowledge')Creates new Tab
Switch sub-page in settingsnavigate({ to: '/settings/provider' })Navigates within current Tab
Open detail from listopenTab('/chat/123', { title: '...' })Creates new Tab
Go back to previous pagenavigate({ to: '..' })Goes back within current Tab

API Reference

useTabs() Return Value

Property/MethodTypeDescription
tabsTab[]List of all Tabs
activeTabIdstringCurrently active Tab ID
activeTabTab | undefinedCurrently active Tab object
openTab(url, options?)(url: string, options?: OpenTabOptions) => stringOpen Tab, returns Tab ID
closeTab(id)(id: string) => voidClose specified Tab
setActiveTab(id)(id: string) => voidSwitch to specified Tab
updateTab(id, updates)(id: string, updates: Partial<Tab>) => voidUpdate Tab properties

OpenTabOptions

OptionTypeDefaultDescription
forceNewbooleanfalseForce create new Tab
titlestringURL pathTab title
type'route' | 'webview''route'Tab type
idstringAuto-generatedCustom Tab ID

Architecture Overview

text
AppShell
├── Sidebar
├── TabBar
└── Content Area
    ├── TabRouter #1 (Home)
    │   └── Activity(visible) → MemoryRouter → RouterProvider
    ├── TabRouter #2 (Settings)
    │   └── Activity(hidden) → MemoryRouter → RouterProvider
    └── WebviewContainer (for webview tabs)
  • Each Tab has its own independent MemoryRouter instance
  • Uses React 19 <Activity> component to control visibility
  • Components are not unmounted on Tab switch, state is fully preserved (KeepAlive)

Error Handling

LayerMechanismScope
Route render errordefaultErrorComponent: RouteErrorFallback on every per-tab router (TabRouter.tsx)Contained to the throwing tab; themed error card with retry/reload
Provider render errorWindow-level <ErrorBoundary fallbackComponent={WindowFatalFallback}> in each window AppWhole window falls back to a context-free fatal page instead of a white screen
  • A specific route can override the default with its own errorComponent route option
  • Without defaultErrorComponent, TanStack wraps matches in a pass-through fragment: a route render error would bubble to the window-level boundary and tear down the whole window

File Structure

text
src/renderer/
├── routes/                    # Route pages (TanStack Router file-based routing)
│   ├── __root.tsx            # Root route (renders Outlet)
│   ├── settings.tsx          # /settings
│   ├── settings.index.tsx    # /settings/ index route (flat dot form — never a bare index.tsx)
│   └── README.md             # This document
├── components/layout/
│   ├── AppShell.tsx          # Main layout (Sidebar + TabBar + Content)
│   └── TabRouter.tsx         # Tab router container (MemoryRouter + Activity)
├── hooks/
│   └── useTabs.ts            # Tab state management hook
└── routeTree.gen.ts          # Auto-generated route tree (do not edit manually)

Important Notes

  1. Do not manually edit routeTree.gen.ts - It is automatically generated by TanStack Router
  2. File name determines route path - routes/settings.tsx/settings
  3. Dynamic parameters use $ - routes/chat/$topicId.tsx/chat/:topicId
  4. Page state is automatically preserved - Tab switching won't lose useState, scroll position, etc.