-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
[WIP] Preact and Vanilla-JS Router adapters #5782
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?
Changes from all commits
ce2f9fa
ed54879
c7eddee
6158676
60871de
cde3483
e79b6a2
011302f
fee888c
92f58fc
3ceea9e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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> |
| 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" | ||
| } | ||
| } |
| 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' | ||
|
|
||
| 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 | ||
| } | ||
| 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) | ||
| } |
| 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) | ||
| } |
| 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>, | ||
| }) |
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix invoice detail link path This link resolves to Apply this diff to point at the correct nested route: - to="/invoices/$invoiceId"
+ to="/_auth/invoices/$invoiceId"📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| </li> | ||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||
| </ol> | ||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||
| <div className="col-span-2 md:col-span-4 py-2 px-4"> | ||||||||||||||||||||||||||||||||||||||||||
| <Outlet /> | ||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainVerify the link paths resolve to the correct nested routes. The Links use absolute paths 🏁 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.tsxLength 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 2Length 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 💡 Result: Short answer
Key points and examples
Sources
🏁 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 1Length of output: 1590 Update the link paths to resolve to nested routes under The links use absolute paths Change the links to either:
🤖 Prompt for AI Agents |
||
| <li> | ||
| <button | ||
| type="button" | ||
| className="hover:underline" | ||
| onClick={handleLogout} | ||
| > | ||
| Logout | ||
| </button> | ||
| </li> | ||
| </ul> | ||
| <hr /> | ||
| <Outlet /> | ||
| </div> | ||
| ) | ||
| } | ||
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.
Fix import sorting to satisfy ESLint.
The imports from
preact/hooksare not alphabetically sorted.useEffectshould come beforeuseCallback.Apply this diff:
📝 Committable suggestion
🧰 Tools
🪛 ESLint
[error] 2-2: Member 'useEffect' of the import declaration should be sorted alphabetically.
(sort-imports)
🤖 Prompt for AI Agents