Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions examples/preact/authenticated-routes/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TanStack Router - Preact Authenticated Routes</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
27 changes: 27 additions & 0 deletions examples/preact/authenticated-routes/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "tanstack-router-preact-example-authenticated-routes",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port 3000",
"build": "vite build && tsc --noEmit",
"serve": "vite preview",
"start": "vite"
},
"dependencies": {
"@tanstack/preact-router": "workspace:*",
"@tanstack/preact-router-devtools": "workspace:*",
"@tanstack/router-plugin": "workspace:*",
"preact": "^10.24.3",
"redaxios": "^0.5.1",
"postcss": "^8.5.1",
"autoprefixer": "^10.4.20",
"tailwindcss": "^3.4.17",
"zod": "^3.24.2"
},
"devDependencies": {
"@preact/preset-vite": "^2.10.2",
"typescript": "^5.7.2",
"vite": "^7.1.7"
}
}
68 changes: 68 additions & 0 deletions examples/preact/authenticated-routes/src/auth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { createContext } from 'preact'
import { useContext, useState, useEffect, useCallback } from 'preact/hooks'
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix import sorting to satisfy ESLint.

The imports from preact/hooks are not alphabetically sorted. useEffect should come before useCallback.

Apply this diff:

-import { useContext, useState, useEffect, useCallback } from 'preact/hooks'
+import { useCallback, useContext, useEffect, useState } from 'preact/hooks'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { useContext, useState, useEffect, useCallback } from 'preact/hooks'
import { useCallback, useContext, useEffect, useState } from 'preact/hooks'
🧰 Tools
🪛 ESLint

[error] 2-2: Member 'useEffect' of the import declaration should be sorted alphabetically.

(sort-imports)

🤖 Prompt for AI Agents
examples/preact/authenticated-routes/src/auth.tsx around line 2: the named
imports from 'preact/hooks' are not in the expected order; move useEffect before
useCallback so the import reads: import { useContext, useState, useEffect,
useCallback } from 'preact/hooks' to satisfy ESLint.


import { sleep } from './utils'

export interface AuthContext {
isAuthenticated: boolean
login: (username: string) => Promise<void>
logout: () => Promise<void>
user: string | null
}

const AuthContext = createContext<AuthContext | null>(null)

const key = 'tanstack.auth.user'

function getStoredUser() {
return localStorage.getItem(key)
}

function setStoredUser(user: string | null) {
if (user) {
localStorage.setItem(key, user)
} else {
localStorage.removeItem(key)
}
}

export function AuthProvider({
children,
}: {
children: preact.ComponentChildren
}) {
const [user, setUser] = useState<string | null>(getStoredUser())
const isAuthenticated = !!user

const logout = useCallback(async () => {
await sleep(250)

setStoredUser(null)
setUser(null)
}, [])

const login = useCallback(async (username: string) => {
await sleep(500)

setStoredUser(username)
setUser(username)
}, [])

useEffect(() => {
setUser(getStoredUser())
}, [])

return (
<AuthContext.Provider value={{ isAuthenticated, user, login, logout }}>
{children}
</AuthContext.Provider>
)
}

export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}
42 changes: 42 additions & 0 deletions examples/preact/authenticated-routes/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { render } from 'preact'
import { RouterProvider, createRouter } from '@tanstack/preact-router'

import { routeTree } from './routeTree.gen'
import { AuthProvider, useAuth } from './auth'
import './styles.css'

// Set up a Router instance
const router = createRouter({
routeTree,
defaultPreload: 'intent',
scrollRestoration: true,
context: {
auth: undefined!, // This will be set after we wrap the app in an AuthProvider
},
})

// Register things for typesafety
declare module '@tanstack/preact-router' {
interface Register {
router: typeof router
}
}

function InnerApp() {
const auth = useAuth()
return <RouterProvider router={router} context={{ auth }} />
}

function App() {
return (
<AuthProvider>
<InnerApp />
</AuthProvider>
)
}

const rootElement = document.getElementById('app')!

if (!rootElement.innerHTML) {
render(<App />, rootElement)
}
23 changes: 23 additions & 0 deletions examples/preact/authenticated-routes/src/posts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import axios from 'redaxios'

export type InvoiceType = {
id: number
title: string
body: string
}

export const fetchInvoices = async () => {
console.info('Fetching invoices...')
await new Promise((r) => setTimeout(r, 500))
return axios
.get<Array<InvoiceType>>('https://jsonplaceholder.typicode.com/posts')
.then((r) => r.data.slice(0, 10))
}

export const fetchInvoiceById = async (id: number) => {
console.info(`Fetching invoice with id ${id}...`)
await new Promise((r) => setTimeout(r, 500))
return axios
.get<InvoiceType>(`https://jsonplaceholder.typicode.com/posts/${id}`)
.then((r) => r.data)
}
17 changes: 17 additions & 0 deletions examples/preact/authenticated-routes/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Outlet, createRootRouteWithContext } from '@tanstack/preact-router'
import { TanStackRouterDevtools } from '@tanstack/preact-router-devtools'

import type { AuthContext } from '../auth'

interface MyRouterContext {
auth: AuthContext
}

export const Route = createRootRouteWithContext<MyRouterContext>()({
component: () => (
<>
<Outlet />
<TanStackRouterDevtools position="bottom-right" initialIsOpen={false} />
</>
),
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createFileRoute } from '@tanstack/preact-router'

import { useAuth } from '../auth'

export const Route = createFileRoute('/_auth/dashboard')({
component: DashboardPage,
})

function DashboardPage() {
const auth = useAuth()

return (
<section className="grid gap-2 p-2">
<p>Hi {auth.user}!</p>
<p>You are currently on the dashboard route.</p>
</section>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createFileRoute } from '@tanstack/preact-router'

import { fetchInvoiceById } from '../posts'

export const Route = createFileRoute('/_auth/invoices/$invoiceId')({
loader: async ({ params: { invoiceId } }) => {
return {
invoice: await fetchInvoiceById(parseInt(invoiceId)),
}
},
component: InvoicePage,
})

function InvoicePage() {
const { invoice } = Route.useLoaderData()

return (
<section className="grid gap-2">
<h2 className="text-lg">
<strong>Invoice No.</strong> #{invoice.id.toString().padStart(2, '0')}
</h2>
<p>
<strong>Invoice title:</strong> {invoice.title}
</p>
<p>
<strong>Invoice body:</strong> {invoice.body}
</p>
</section>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createFileRoute } from '@tanstack/preact-router'

export const Route = createFileRoute('/_auth/invoices/')({
component: () => <div>Select an invoice to view it!</div>,
})
42 changes: 42 additions & 0 deletions examples/preact/authenticated-routes/src/routes/_auth.invoices.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Link, Outlet, createFileRoute } from '@tanstack/preact-router'

import { fetchInvoices } from '../posts'

export const Route = createFileRoute('/_auth/invoices')({
loader: async () => ({
invoices: await fetchInvoices(),
}),
component: InvoicesRoute,
})

function InvoicesRoute() {
const { invoices } = Route.useLoaderData()

return (
<div className="grid grid-cols-3 md:grid-cols-5 min-h-[500px]">
<div className="col-span-1 py-2 pl-2 pr-4 md:border-r">
<p className="mb-2">Choose an invoice from the list below.</p>
<ol className="grid gap-2">
{invoices.map((invoice) => (
<li key={invoice.id}>
<Link
to="/invoices/$invoiceId"
params={{ invoiceId: invoice.id.toString() }}
className="text-blue-600 hover:opacity-75"
activeProps={{ className: 'font-bold underline' }}
>
<span className="tabular-nums">
#{invoice.id.toString().padStart(2, '0')}
</span>{' '}
- {invoice.title.slice(0, 16)}...
</Link>
Comment on lines +23 to +32
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix invoice detail link path

This link resolves to /invoices/..., which skips the /_auth segment. As a result the nested invoice route at /_auth/invoices/$invoiceId never matches and users hit a 404 instead of the detail page.

Apply this diff to point at the correct nested route:

-                to="/invoices/$invoiceId"
+                to="/_auth/invoices/$invoiceId"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
to="/invoices/$invoiceId"
params={{ invoiceId: invoice.id.toString() }}
className="text-blue-600 hover:opacity-75"
activeProps={{ className: 'font-bold underline' }}
>
<span className="tabular-nums">
#{invoice.id.toString().padStart(2, '0')}
</span>{' '}
- {invoice.title.slice(0, 16)}...
</Link>
to="/_auth/invoices/$invoiceId"
params={{ invoiceId: invoice.id.toString() }}
className="text-blue-600 hover:opacity-75"
activeProps={{ className: 'font-bold underline' }}
>
<span className="tabular-nums">
#{invoice.id.toString().padStart(2, '0')}
</span>{' '}
- {invoice.title.slice(0, 16)}...
</Link>
🤖 Prompt for AI Agents
In examples/preact/authenticated-routes/src/routes/_auth.invoices.tsx around
lines 24-33 the Link uses an absolute path "/invoices/$invoiceId" which bypasses
the parent /_auth segment and breaks nested routing; change the Link's to value
to a relative nested path (remove the leading slash, e.g. "invoices/$invoiceId"
or "./invoices/$invoiceId") so the route resolves under /_auth, keeping params
as invoiceId.toString().

</li>
))}
</ol>
</div>
<div className="col-span-2 md:col-span-4 py-2 px-4">
<Outlet />
</div>
</div>
)
}
75 changes: 75 additions & 0 deletions examples/preact/authenticated-routes/src/routes/_auth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {
Link,
Outlet,
createFileRoute,
redirect,
useRouter,
} from '@tanstack/preact-router'

import { useAuth } from '../auth'

export const Route = createFileRoute('/_auth')({
beforeLoad: ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: {
redirect: location.href,
},
})
}
},
component: AuthLayout,
})

function AuthLayout() {
const router = useRouter()
const navigate = Route.useNavigate()
const auth = useAuth()

const handleLogout = () => {
if (window.confirm('Are you sure you want to logout?')) {
auth.logout().then(() => {
router.invalidate().finally(() => {
navigate({ to: '/' })
})
})
}
}

return (
<div className="p-2 h-full">
<h1>Authenticated Route</h1>
<p>This route's content is only visible to authenticated users.</p>
<ul className="py-2 flex gap-2">
<li>
<Link
to="/dashboard"
className="hover:underline data-[status='active']:font-semibold"
>
Dashboard
</Link>
</li>
<li>
<Link
to="/invoices"
className="hover:underline data-[status='active']:font-semibold"
>
Invoices
</Link>
</li>
Comment on lines +46 to +60
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the link paths resolve to the correct nested routes.

The Links use absolute paths /dashboard and /invoices, which may not correctly resolve to the nested routes under /_auth. Based on the file structure, these routes are likely at /_auth/dashboard and /_auth/invoices.


🏁 Script executed:

#!/bin/bash
# Description: Verify the route structure and check for dashboard and invoices routes

# Find route files for dashboard and invoices
fd -e tsx -e ts 'dashboard|invoices' examples/preact/authenticated-routes/src/routes/

# Check the route definitions to see their registered paths
rg -nP --type=tsx --type=ts "createFileRoute\(['\"].*(?:dashboard|invoices)" examples/preact/authenticated-routes/src/routes/

Length of output: 313


🏁 Script executed:

#!/bin/bash
# Inspect the _auth.tsx file to see the actual Link implementation
cat examples/preact/authenticated-routes/src/routes/_auth.tsx

Length of output: 1627


🏁 Script executed:

#!/bin/bash
# Search for Link component imports to understand its origin
rg -n "import.*Link" examples/preact/authenticated-routes/src/routes/_auth.tsx -A 2 -B 2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there's a dashboard route at root level
fd -e tsx -e ts '\bdashboard\.tsx\b' examples/preact/authenticated-routes/src/routes/ | grep -v '_auth'

Length of output: 0



---

🌐 Web query:

```web
TanStack Router Link absolute path resolution nested routes

💡 Result:

Short answer

  • TanStack Router Link (and the shared navigation API) treats all navigations as relative by concept, but Link 'to' values are treated as absolute unless you provide a from (origin) route path or use explicit relative syntax (./, ../). Provide from to make a relative link resolve against a specific route. [1][2]

Key points and examples

  • Default = absolute: always resolves to /about regardless of your current nested route. [1]
  • Make a relative link: give a from (route.fullPath or route id) and a relative to: . This resolves ../categories relative to the provided from. If no from is given, relative paths resolve from the current active location. [1][2]
  • Pathless layouts: a pathless (no-path) layout has no path of its own, so the router treats its from location as the parent — this can change how relative links resolve. [1][2]
  • Imperative navigation caveat: some versions had bugs (e.g., useNavigate / absolute paths stripping basepath) — if you rely on basepath behavior test with your router version or check the GitHub issues for fixes. [3]

Sources

  • TanStack Router navigation docs (React): Navigation / Relative Links / ToOptions. [1]
  • TanStack Router docs (Shared navigation text, Solid guide): Everything is relative / examples for from + to. [2]
  • GitHub issue re: useNavigate and basepath edge-case. [3]

🏁 Script executed:

#!/bin/bash
# Verify the route structure by listing all route files to confirm nesting
ls -la examples/preact/authenticated-routes/src/routes/ | grep -E '_auth\.(dashboard|invoices)'

Length of output: 289


🏁 Script executed:

#!/bin/bash
# Check if there are any tests or documentation that show expected navigation behavior
rg -n "to=\".*dashboard|to=\".*invoices" examples/preact/authenticated-routes/ -A 1 -B 1

Length of output: 1590


Update the link paths to resolve to nested routes under /_auth.

The links use absolute paths /dashboard and /invoices, but the routes exist as nested routes at /_auth/dashboard and /_auth/invoices. TanStack Router treats to values as absolute by default, so these links won't resolve correctly.

Change the links to either:

  • Absolute paths: to="/_auth/dashboard" and to="/_auth/invoices", or
  • Relative paths: to="./dashboard" and to="./invoices"
🤖 Prompt for AI Agents
In examples/preact/authenticated-routes/src/routes/_auth.tsx around lines 46 to
60, the Link components point to absolute paths (/dashboard and /invoices) but
the routes are nested under /_auth so these links will not resolve; update the
to props to reference the nested routes either by using absolute nested paths
to="/_auth/dashboard" and to="/_auth/invoices" or by using relative paths
to="./dashboard" and to="./invoices" so the links correctly navigate to the
nested routes.

<li>
<button
type="button"
className="hover:underline"
onClick={handleLogout}
>
Logout
</button>
</li>
</ul>
<hr />
<Outlet />
</div>
)
}
Loading
Loading