Skip to content

Commit 895bc58

Browse files
authored
Merge branch 'main' into blog-router-refactor-signals
2 parents ceec1ad + b4d12a1 commit 895bc58

36 files changed

+975
-374
lines changed

media/brand.sketch

171 KB
Binary file not shown.

public/blog-assets/tanstack-router-route-matching-tree-rewrite/header.png renamed to public/blog-assets/tanstack-router-route-matching-tree-rewrite/big-number.png

File renamed without changes.
290 KB
Loading

public/fonts/Inter-latin-ext.woff2

130 KB
Binary file not shown.

public/fonts/Inter-latin.woff2

71.2 KB
Binary file not shown.

public/fonts/Inter.woff2

-344 KB
Binary file not shown.

src/blog/tanstack-router-route-matching-tree-rewrite.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ authors:
66
- Florian Pellet
77
---
88

9-
![Big performance number](/blog-assets/tanstack-router-route-matching-tree-rewrite/header.png)
9+
![Fantasy landscape illustration used as the article header](/blog-assets/tanstack-router-route-matching-tree-rewrite/header.jpg)
1010

1111
We achieved a 20,000× performance improvement in route matching in TanStack Router. Let's be honest, this is _definitely_ cherry-picked, but the number is real and comes from a real production application. More importantly, it shows that matching a pathname to a route is no longer bottlenecked by the number of routes in your application.
1212

13+
![Big performance number](/blog-assets/tanstack-router-route-matching-tree-rewrite/big-number.png)
14+
1315
## The Real Problem: correctness, not speed
1416

1517
One big responsibility of a router is to match a given URL pathname (e.g., `/users/123`) to a route definition (e.g., `/users/$userId`). This is deceptively complex when you consider all the different types of route segments (static, dynamic, optional, wildcard) and the priority rules that govern which route should match first.

src/components/CopyPageDropdown.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ import {
1010
DropdownContent,
1111
DropdownItem,
1212
} from './Dropdown'
13-
import {
14-
getPackageManager,
15-
PACKAGE_MANAGERS,
16-
} from '~/utils/markdown/installCommand'
13+
import { getPackageManager } from '~/utils/markdown/installCommand'
1714

1815
// Markdown icon component matching the screenshot
1916
function MarkdownIcon({ className }: { className?: string }) {

src/components/DocsLayout.tsx

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -534,28 +534,59 @@ export function DocsLayout({
534534
d.status === 'active' && d.name !== 'Nozzle.io' && d.id !== 'fireship',
535535
)
536536

537-
const menuItems = menuConfig.map((group, i) => {
538-
const WrapperComp = group.collapsible ? 'details' : 'div'
539-
const LabelComp = group.collapsible ? 'summary' : 'div'
537+
const groupInitialOpenState = React.useMemo(() => {
538+
return menuConfig.reduce<Record<string, boolean>>((acc, group, index) => {
539+
const isChildActive = group.children.some((child) => child.to === _splat)
540+
const key = `${index}:${String(group.label)}`
540541

541-
const isChildActive = group.children.some((d) => d.to === _splat)
542-
const configGroupOpenState =
543-
typeof group.defaultCollapsed !== 'undefined'
544-
? !group.defaultCollapsed // defaultCollapsed is true means the group is closed
545-
: undefined
546-
const isOpen = isChildActive ? true : (configGroupOpenState ?? false)
542+
acc[key] = isChildActive
543+
? true
544+
: typeof group.defaultCollapsed !== 'undefined'
545+
? !group.defaultCollapsed
546+
: false
547547

548-
const detailsProps = group.collapsible ? { open: isOpen } : {}
548+
return acc
549+
}, {})
550+
}, [menuConfig, _splat])
549551

550-
return (
551-
<WrapperComp
552-
key={`group-${i}`}
553-
className="[&>summary]:before:mr-1 [&>summary]:marker:text-[0.8em] [&>summary]:marker:leading-4 relative select-none"
554-
{...detailsProps}
555-
>
556-
<LabelComp className="text-[.8em] font-bold leading-4 px-2 ts-sidebar-label">
557-
{group?.label}
558-
</LabelComp>
552+
const [openGroups, setOpenGroups] = React.useState(groupInitialOpenState)
553+
554+
React.useEffect(() => {
555+
setOpenGroups((prev) => {
556+
let hasChanged = false
557+
const next = { ...prev }
558+
559+
Object.entries(groupInitialOpenState).forEach(([key, isOpen]) => {
560+
if (!(key in next)) {
561+
next[key] = isOpen
562+
hasChanged = true
563+
return
564+
}
565+
566+
if (isOpen && !next[key]) {
567+
next[key] = true
568+
hasChanged = true
569+
}
570+
})
571+
572+
return hasChanged ? next : prev
573+
})
574+
}, [groupInitialOpenState])
575+
576+
const menuItems = menuConfig.map((group, i) => {
577+
const groupKey = `${i}:${String(group.label)}`
578+
579+
const groupContent = (
580+
<>
581+
{group.collapsible ? (
582+
<summary className="text-[.8em] font-bold leading-4 px-2 ts-sidebar-label">
583+
{group.label}
584+
</summary>
585+
) : (
586+
<div className="text-[.8em] font-bold leading-4 px-2 ts-sidebar-label">
587+
{group.label}
588+
</div>
589+
)}
559590
<div className="h-2" />
560591
<ul className="text-[.85em] leading-snug list-none">
561592
{group?.children?.map((child, i) => {
@@ -580,6 +611,7 @@ export function DocsLayout({
580611
onClick={() => {
581612
detailsRef.current.removeAttribute('open')
582613
}}
614+
preload={false}
583615
activeOptions={{
584616
exact: true,
585617
includeHash: false,
@@ -614,7 +646,32 @@ export function DocsLayout({
614646
)
615647
})}
616648
</ul>
617-
</WrapperComp>
649+
</>
650+
)
651+
652+
return group.collapsible ? (
653+
<details
654+
key={`group-${i}`}
655+
className="[&>summary]:before:mr-1 [&>summary]:marker:text-[0.8em] [&>summary]:marker:leading-4 relative select-none"
656+
open={openGroups[groupKey] ?? false}
657+
onToggle={(event) => {
658+
const nextOpen = event.currentTarget.open
659+
setOpenGroups((prev) =>
660+
prev[groupKey] === nextOpen
661+
? prev
662+
: { ...prev, [groupKey]: nextOpen },
663+
)
664+
}}
665+
>
666+
{groupContent}
667+
</details>
668+
) : (
669+
<div
670+
key={`group-${i}`}
671+
className="[&>summary]:before:mr-1 [&>summary]:marker:text-[0.8em] [&>summary]:marker:leading-4 relative select-none"
672+
>
673+
{groupContent}
674+
</div>
618675
)
619676
})
620677

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from 'react'
2+
import type { Framework } from '~/libraries'
3+
import { useIntersectionObserver } from '~/hooks/useIntersectionObserver'
4+
5+
const LazyCodeExampleCardImpl = React.lazy(async () => {
6+
const mod = await import('./CodeExampleCard')
7+
8+
return { default: mod.CodeExampleCard }
9+
})
10+
11+
interface LazyCodeExampleCardProps {
12+
title?: string
13+
frameworks: Framework[]
14+
codeByFramework: Partial<Record<Framework, { lang: string; code: string }>>
15+
}
16+
17+
export function LazyCodeExampleCard(props: LazyCodeExampleCardProps) {
18+
const { ref, isIntersecting } = useIntersectionObserver({
19+
rootMargin: '25%',
20+
triggerOnce: true,
21+
})
22+
23+
return (
24+
<div ref={ref} className="px-4 space-y-4 flex flex-col items-center">
25+
{!isIntersecting ? (
26+
<div className="w-full max-w-5xl mx-auto space-y-4">
27+
<div className="h-10 w-56 mx-auto rounded bg-gray-200/70 dark:bg-gray-800/70 animate-pulse" />
28+
<div className="rounded-xl border border-gray-200 dark:border-gray-800 bg-white/70 dark:bg-gray-900/70 overflow-hidden">
29+
<div className="h-14 border-b border-gray-200 dark:border-gray-800 bg-gray-100/70 dark:bg-gray-800/70 animate-pulse" />
30+
<div className="p-4 space-y-3">
31+
<div className="h-4 w-11/12 rounded bg-gray-200/70 dark:bg-gray-800/70 animate-pulse" />
32+
<div className="h-4 w-10/12 rounded bg-gray-200/70 dark:bg-gray-800/70 animate-pulse" />
33+
<div className="h-4 w-8/12 rounded bg-gray-200/70 dark:bg-gray-800/70 animate-pulse" />
34+
<div className="h-4 w-9/12 rounded bg-gray-200/70 dark:bg-gray-800/70 animate-pulse" />
35+
<div className="h-4 w-7/12 rounded bg-gray-200/70 dark:bg-gray-800/70 animate-pulse" />
36+
<div className="h-48 rounded bg-gray-100/70 dark:bg-gray-800/50 animate-pulse" />
37+
</div>
38+
</div>
39+
</div>
40+
) : (
41+
<React.Suspense fallback={null}>
42+
<LazyCodeExampleCardImpl {...props} />
43+
</React.Suspense>
44+
)}
45+
</div>
46+
)
47+
}

0 commit comments

Comments
 (0)