<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ tanstack - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ tanstack - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 31 May 2026 09:38:41 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/tanstack/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build an Admin Dashboard with shadcn/ui and TanStack Start ]]>
                </title>
                <description>
                    <![CDATA[ In this guide, we’ll build a feature-rich admin dashboard using shadcn/ui for beautiful, reusable components and TanStack Start for a powerful, type-safe full-stack framework. By the end, you’ll have: A fully functional /dashboard layout A statisti... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-an-admin-dashboard-with-shadcnui-and-tanstack-start/</link>
                <guid isPermaLink="false">6931bd617fcd342128f08ed6</guid>
                
                    <category>
                        <![CDATA[ shadcn ]]>
                    </category>
                
                    <category>
                        <![CDATA[ shadcnui ]]>
                    </category>
                
                    <category>
                        <![CDATA[ shadcn ui ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tanstack-start ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tanstack ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ajay Patel ]]>
                </dc:creator>
                <pubDate>Thu, 04 Dec 2025 16:57:05 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1764780775287/b8cb826d-ac42-497c-8bb9-b9ffe797df83.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this guide, we’ll build a feature-rich admin dashboard using shadcn/ui for beautiful, reusable components and TanStack Start for a powerful, type-safe full-stack framework.</p>
<p>By the end, you’ll have:</p>
<ul>
<li><p>A fully functional <code>/dashboard</code> layout</p>
</li>
<li><p>A statistics-rich dashboard home page with charts and tables</p>
</li>
<li><p>A Products page using TanStack Query and TanStack Table</p>
</li>
<li><p>A Settings page with profile and notification controls</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764155564957/eda17d57-3f13-4526-be89-be55ec27453c.png" alt="TanStack Start dashboard" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-why-tanstack-start">Why TanStack Start?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-shadcnui">Why shadcn/ui?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-build-the-admin-dashboard-using-shadcnui-and-tanstack-start">How to Build the Admin Dashboard Using shadcn/ui and TanStack Start</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-create-a-new-tanstack-app">1. Create a new TanStack app</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-initial-cleanup">2. Initial Cleanup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-setting-up-shadcnstudio-blocks">3. Setting Up shadcn/studio Blocks</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-routing-structure-for-the-dashboard">4. Routing Structure for the Dashboard</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-5-creating-the-dashboard-layout">5. Creating the /dashboard Layout</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-6-building-the-dashboard-home-page">6. Building the Dashboard Home Page</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-7-set-up-the-products-page">7. Set up the Products Page.</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-8-settings-page">8. Settings Page</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-live-demo-amp-source-code">Live Demo &amp; Source Code</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-summary">Summary</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-whats-next">What’s Next?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-resources">Resources:</a></p>
</li>
</ul>
</li>
</ol>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>Before we start the guide, let’s understand the basic requirements of the project:</p>
<ul>
<li><p>Node.js 18+ installed</p>
</li>
<li><p>Basic knowledge of React and TypeScript</p>
</li>
<li><p>Familiarity with TailwindCSS</p>
</li>
</ul>
<h3 id="heading-what-we-will-build">What we will build</h3>
<p>In this article, we’ll build a fully functional admin dashboard with three main sections:</p>
<ol>
<li><p><strong>Dashboard overview</strong>: A home page that displays various charts showing sales metrics, product insights widgets, and a transaction history table.</p>
</li>
<li><p><strong>Products:</strong> A product page that demonstrates data fetching, server-side pagination, and advanced table features like column searching, sorting, and column filtering.</p>
</li>
<li><p><strong>Settings:</strong> A user-friendly settings page with profile management and notification preferences.</p>
</li>
</ol>
<p>The dashboard will include a responsive sidebar navigation, breadcrumb trails, a user profile dropdown, and a language selector.</p>
<h2 id="heading-why-tanstack-start">Why TanStack Start?</h2>
<p><a target="_blank" href="https://tanstack.com/start/latest">TanStack Start</a> is a modern full-stack React framework built on top of TanStack Router. It aims to be a flexible, type-safe alternative to traditional meta-frameworks like Next.js.</p>
<p>Some key benefits of TanStack Start include:</p>
<ul>
<li><p>Type-safe routing and data loading</p>
</li>
<li><p>Server-side rendering (SSR) out of the box</p>
</li>
<li><p>Built on TanStack Router, with file-based routing</p>
</li>
<li><p>Great DX with TypeScript and TanStack Query integration</p>
</li>
</ul>
<p>We’ll pair it with shadcn/ui to quickly build a polished admin dashboard.</p>
<h2 id="heading-why-shadcnui">Why shadcn/ui?</h2>
<p><a target="_blank" href="https://ui.shadcn.com/">shadcn/ui</a> is a collection of beautifully designed, accessible React components built on top of Radix UI and styled with Tailwind CSS.</p>
<p>Instead of installing a package, you can copy and paste the component's code directly into your project or use a CLI to generate it. This gives you full control over the code structure &amp; styling. This approach makes Shadcn highly customizable for frameworks like TanStack Start, Next.js, Astro, and so on.</p>
<h2 id="heading-how-to-build-the-admin-dashboard-using-shadcnui-and-tanstack-start">How to Build the Admin Dashboard Using shadcn/ui and TanStack Start</h2>
<h3 id="heading-1-create-a-new-tanstack-app">1. Create a new TanStack app</h3>
<p>To get started, you’ll need to create a new TanStack Start app. You can do that with the following command:</p>
<pre><code class="lang-typescript">pnpm create <span class="hljs-meta">@tanstack</span>/start<span class="hljs-meta">@latest</span>
</code></pre>
<p>During the CLI setup, when it asks about add-ons, make sure to select:</p>
<ul>
<li><p>Shadcn</p>
</li>
<li><p>Table</p>
</li>
<li><p>Query</p>
</li>
</ul>
<p>These will give you the shadcn/ui setup and the TanStack Query + Table integrations we’ll use later.</p>
<h3 id="heading-2-initial-cleanup">2. Initial Cleanup</h3>
<p>TanStack Start’s starter template comes with some demo routes and a header we don’t need.</p>
<p>Clean up the project as follows:</p>
<ol>
<li><p>Remove the demo folder inside the <code>src/routes</code> directory (or wherever your router directory lives).</p>
</li>
<li><p>Delete <code>Header.tsx</code> from <code>src/components</code>.</p>
</li>
<li><p>Remove the <code>Header</code> import and usage from <code>src/routes/__root.tsx</code>.</p>
</li>
<li><p>Clean up the <code>src/routes/index.tsx</code> file to something minimal (or leave a simple landing page).</p>
</li>
</ol>
<p>At this point, you can make the initial commit to your repo.</p>
<h3 id="heading-3-setting-up-shadcnstudio-blocks">3. Setting Up shadcn/studio Blocks</h3>
<p>Before we set up, let’s make sure you’re clear on what the shadcn/studio and Shadcn registries are.</p>
<h4 id="heading-what-is-shadcnstudio">What is shadcn/studio?</h4>
<p><a target="_blank" href="https://shadcnstudio.com">shadcn/studio</a> is an open-source collection of copy-and-paste shadcn/ui components, blocks, and templates. It’s paired with a powerful shadcn theme generator to help you craft, customize, and ship faster.</p>
<h4 id="heading-what-is-shadcn-registry">What is Shadcn Registry?</h4>
<p>A shadcn registry is a system for sharing and distributing reusable code assets such as UI components, hooks, and theme configurations across different projects. Running your own registry allows you to publish your custom components that others can then use. The registry uses a <code>registry.json</code> file to define and organize the components and their associated files. </p>
<p>If you want to know more about registries, you can refer to the <a target="_blank" href="https://ui.shadcn.com/docs/registry">official documentation here</a>.</p>
<p>For quick building, we will use shadcn/studio’s free shadcn block – dashboard shell.</p>
<p>First, configure the registries in your <code>components.json</code>:</p>
<pre><code class="lang-typescript">{
  <span class="hljs-comment">// ...existing config</span>
  <span class="hljs-string">"registries"</span>: {
    <span class="hljs-string">"@shadcn-studio"</span>: <span class="hljs-string">"https://shadcnstudio.com/r/{name}.json"</span>,
    <span class="hljs-string">"@ss-components"</span>: <span class="hljs-string">"https://shadcnstudio.com/r/components/{name}.json"</span>,
    <span class="hljs-string">"@ss-blocks"</span>: <span class="hljs-string">"https://shadcnstudio.com/r/blocks/{name}.json"</span>,
    <span class="hljs-string">"@ss-themes"</span>: <span class="hljs-string">"https://shadcnstudio.com/r/themes/{name}.json"</span>
  }
}
</code></pre>
<p>If you face any issues while setting up, you can refer to the <a target="_blank" href="https://shadcnstudio.com/docs/getting-started/how-to-use-shadcn-cli">docs</a>.</p>
<h4 id="heading-install-the-dashboard-shell-block">Install the Dashboard Shell Block</h4>
<p>To get started, visit <a target="_blank" href="https://shadcnstudio.com/blocks">Shadcn blocks</a> and navigate to the Dashboard and App section. Then select the <a target="_blank" href="https://shadcnstudio.com/blocks/dashboard-and-application/dashboard-shell#dashboard-shell-1">Dashboard Shell 1</a> block (it’s free to use).</p>
<p>On the top-right, you’ll see a command to install the block into your project:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764155098742/23d1bee2-e082-4b19-860a-8112fe6bf41c.png" alt="shadcn/stuidio dashboard shell " class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Copy that command, paste it into your terminal, and run it. This will install all the components needed for the dashboard layout (sidebar, header, dropdowns, and so on).</p>
<h3 id="heading-4-routing-structure-for-the-dashboard">4. Routing Structure for the Dashboard</h3>
<p>Next, we’ll set up the dashboard routes.</p>
<p>First, create a new layout route for <code>/dashboard</code> by adding a file at:</p>
<p><code>src/routes/dashboard.tsx</code></p>
<p>Then, inside a <code>dashboard</code> directory, create the three pages that will live under this layout:</p>
<ul>
<li><p><code>src/routes/dashboard/index.tsx</code> – main dashboard overview</p>
</li>
<li><p><code>src/routes/dashboard/products.tsx</code> – products table page</p>
</li>
<li><p><code>src/routes/dashboard/settings.tsx</code> – settings page</p>
</li>
</ul>
<p>Your <code>routes</code> folder should look like this:</p>
<pre><code class="lang-typescript">src/routes/
├── __root.tsx
├── index.tsx
├── dashboard.tsx          <span class="hljs-comment">// Layout for all /dashboard/* pages</span>
└── dashboard/
    ├── index.tsx          <span class="hljs-comment">// /dashboard</span>
    ├── products.tsx       <span class="hljs-comment">// /dashboard/products</span>
    └── settings.tsx       <span class="hljs-comment">// /dashboard/settings</span>
</code></pre>
<h3 id="heading-5-creating-the-dashboard-layout">5. Creating the <code>/dashboard</code> Layout</h3>
<p>This will set up the layout for the dashboard. Create <code>src/routes/dashboard.tsx</code> and paste:</p>
<p>file: <code>src/routes/dashboard.tsx</code></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> LanguageDropdown <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/shadcn-studio/blocks/dropdown-language'</span>
<span class="hljs-keyword">import</span> ProfileDropdown <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/shadcn-studio/blocks/dropdown-profile'</span>
<span class="hljs-keyword">import</span> { Avatar, AvatarImage } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/ui/avatar'</span>
<span class="hljs-keyword">import</span> {
    Breadcrumb,
    BreadcrumbItem,
    BreadcrumbLink,
    BreadcrumbList,
    BreadcrumbPage,
    BreadcrumbSeparator
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/ui/breadcrumb'</span>
<span class="hljs-keyword">import</span> { Button } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/ui/button'</span>
<span class="hljs-keyword">import</span> { Separator } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/ui/separator'</span>
<span class="hljs-keyword">import</span> {
    Sidebar,
    SidebarContent,
    SidebarGroup,
    SidebarGroupContent,
    SidebarGroupLabel,
    SidebarHeader,
    SidebarMenu,
    SidebarMenuButton,
    SidebarMenuItem,
    SidebarProvider,
    SidebarTrigger
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/ui/sidebar'</span>
<span class="hljs-keyword">import</span> { createFileRoute, Link, Outlet, useLocation } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tanstack/react-router'</span>
<span class="hljs-keyword">import</span> {
    FacebookIcon,
    InstagramIcon,
    LanguagesIcon,
    LayoutDashboard,
    LinkedinIcon,
    LogIn,
    Package,
    Settings,
    TwitterIcon,
    User2
} <span class="hljs-keyword">from</span> <span class="hljs-string">'lucide-react'</span>
<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Route = createFileRoute(<span class="hljs-string">'/dashboard'</span>)({
    component: DashboardLayout
})

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">DashboardLayout</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> location = useLocation()
    <span class="hljs-keyword">const</span> pathSegments = location.pathname.split(<span class="hljs-string">'/'</span>).filter(<span class="hljs-built_in">Boolean</span>)

    <span class="hljs-keyword">return</span> (
        &lt;div className=<span class="hljs-string">'flex min-h-dvh w-full'</span>&gt;
            &lt;SidebarProvider&gt;
                &lt;Sidebar&gt;
                    &lt;SidebarContent&gt;
                        &lt;SidebarHeader&gt;
                            &lt;SidebarMenu&gt;
                                &lt;SidebarMenuItem&gt;
                                    &lt;SidebarMenuButton size=<span class="hljs-string">"lg"</span> asChild&gt;
                                        &lt;Link to=<span class="hljs-string">"/"</span>&gt;
                                            &lt;div className=<span class="hljs-string">"flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground"</span>&gt;
                                                &lt;User2 className=<span class="hljs-string">"size-4"</span> /&gt;
                                            &lt;/div&gt;
                                            &lt;div className=<span class="hljs-string">"grid flex-1 text-left text-sm leading-tight"</span>&gt;
                                                &lt;span className=<span class="hljs-string">"truncate font-semibold"</span>&gt;Your App&lt;/span&gt;
                                                &lt;span className=<span class="hljs-string">"truncate text-xs"</span>&gt;Dashboard&lt;/span&gt;
                                            &lt;/div&gt;
                                        &lt;/Link&gt;
                                    &lt;/SidebarMenuButton&gt;
                                &lt;/SidebarMenuItem&gt;
                            &lt;/SidebarMenu&gt;
                        &lt;/SidebarHeader&gt;

                        &lt;SidebarGroup&gt;
                            &lt;SidebarGroupLabel&gt;General&lt;/SidebarGroupLabel&gt;
                            &lt;SidebarGroupContent&gt;
                                &lt;SidebarMenu&gt;
                                    &lt;SidebarMenuItem&gt;
                                        &lt;SidebarMenuButton asChild&gt;
                                            &lt;Link to=<span class="hljs-string">'/dashboard'</span>&gt;
                                                &lt;LayoutDashboard /&gt;
                                                &lt;span&gt;Dashboard&lt;/span&gt;
                                            &lt;/Link&gt;
                                        &lt;/SidebarMenuButton&gt;
                                    &lt;/SidebarMenuItem&gt;
                                    &lt;SidebarMenuItem&gt;
                                        &lt;SidebarMenuButton asChild&gt;
                                            &lt;Link to=<span class="hljs-string">'/dashboard/products'</span>&gt;
                                                &lt;Package /&gt;
                                                &lt;span&gt;Products&lt;/span&gt;
                                            &lt;/Link&gt;
                                        &lt;/SidebarMenuButton&gt;
                                    &lt;/SidebarMenuItem&gt;
                                    &lt;SidebarMenuItem&gt;
                                        &lt;SidebarMenuButton asChild&gt;
                                            &lt;Link to=<span class="hljs-string">'/dashboard/settings'</span>&gt;
                                                &lt;Settings /&gt;
                                                &lt;span&gt;Settings&lt;/span&gt;
                                            &lt;/Link&gt;
                                        &lt;/SidebarMenuButton&gt;
                                    &lt;/SidebarMenuItem&gt;
                                &lt;/SidebarMenu&gt;
                            &lt;/SidebarGroupContent&gt;
                        &lt;/SidebarGroup&gt;
                    &lt;/SidebarContent&gt;
                &lt;/Sidebar&gt;
                &lt;div className=<span class="hljs-string">'flex flex-1 flex-col'</span>&gt;
                    &lt;header className=<span class="hljs-string">'bg-card sticky top-0 z-50 border-b'</span>&gt;
                        &lt;div className=<span class="hljs-string">'mx-auto flex max-w-7xl items-center justify-between gap-6 px-4 py-2 sm:px-6'</span>&gt;
                            &lt;div className=<span class="hljs-string">'flex items-center gap-4'</span>&gt;
                                &lt;SidebarTrigger className=<span class="hljs-string">'[&amp;_svg]:h-5 [&amp;_svg]:w-5'</span> /&gt;
                                &lt;Separator orientation=<span class="hljs-string">'vertical'</span> className=<span class="hljs-string">'hidden h-4 sm:block'</span> /&gt;
                                &lt;Breadcrumb className=<span class="hljs-string">'hidden sm:block'</span>&gt;
                                    &lt;BreadcrumbList&gt;
                                        &lt;BreadcrumbItem&gt;
                                            &lt;BreadcrumbLink asChild&gt;
                                                &lt;Link to=<span class="hljs-string">'/'</span>&gt;Home&lt;/Link&gt;
                                            &lt;/BreadcrumbLink&gt;
                                        &lt;/BreadcrumbItem&gt;
                                        &lt;BreadcrumbSeparator /&gt;
                                        {pathSegments.map(<span class="hljs-function">(<span class="hljs-params">segment, index</span>) =&gt;</span> {
                                            <span class="hljs-keyword">const</span> path = <span class="hljs-string">`/<span class="hljs-subst">${pathSegments.slice(<span class="hljs-number">0</span>, index + <span class="hljs-number">1</span>).join(<span class="hljs-string">'/'</span>)}</span>`</span>
                                            <span class="hljs-keyword">const</span> isLast = index === pathSegments.length - <span class="hljs-number">1</span>
                                            <span class="hljs-keyword">const</span> title = segment.charAt(<span class="hljs-number">0</span>).toUpperCase() + segment.slice(<span class="hljs-number">1</span>)

                                            <span class="hljs-keyword">return</span> (
                                                &lt;React.Fragment key={path}&gt;
                                                    &lt;BreadcrumbItem&gt;
                                                        {isLast ? (
                                                            &lt;BreadcrumbPage&gt;{title}&lt;/BreadcrumbPage&gt;
                                                        ) : (
                                                            &lt;BreadcrumbLink asChild&gt;
                                                                &lt;Link to={path <span class="hljs-keyword">as</span> <span class="hljs-built_in">any</span>}&gt;{title}&lt;/Link&gt;
                                                            &lt;/BreadcrumbLink&gt;
                                                        )}
                                                    &lt;/BreadcrumbItem&gt;
                                                    {!isLast &amp;&amp; &lt;BreadcrumbSeparator /&gt;}
                                                &lt;/React.Fragment&gt;
                                            )
                                        })}
                                    &lt;/BreadcrumbList&gt;
                                &lt;/Breadcrumb&gt;
                            &lt;/div&gt;
                            &lt;div className=<span class="hljs-string">'flex items-center gap-1.5'</span>&gt;
                                &lt;LanguageDropdown
                                    trigger={
                                        &lt;Button variant=<span class="hljs-string">'ghost'</span> size=<span class="hljs-string">'icon'</span>&gt;
                                            &lt;LanguagesIcon /&gt;
                                        &lt;/Button&gt;
                                    }
                                /&gt;
                                &lt;ProfileDropdown
                                    trigger={
                                        &lt;Button variant=<span class="hljs-string">'ghost'</span> size=<span class="hljs-string">'icon'</span> className=<span class="hljs-string">'h-10 w-10'</span>&gt;
                                            &lt;Avatar className=<span class="hljs-string">'h-10 w-10 rounded-md'</span>&gt;
                                                &lt;AvatarImage src=<span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-1.png'</span> /&gt;
                                            &lt;/Avatar&gt;
                                        &lt;/Button&gt;
                                    }
                                /&gt;
                            &lt;/div&gt;
                        &lt;/div&gt;
                    &lt;/header&gt;
                    &lt;main className=<span class="hljs-string">'mx-auto w-full max-w-7xl flex-1 px-4 py-6 sm:px-6'</span>&gt;
                        &lt;Outlet /&gt;
                    &lt;/main&gt;
                    &lt;footer&gt;
                        &lt;div className=<span class="hljs-string">'text-muted-foreground mx-auto flex w-full items-center justify-between gap-3 px-4 py-3 flex-col sm:flex-row sm:gap-6 sm:px-6'</span>&gt;
                            &lt;p className=<span class="hljs-string">'text-sm text-center sm:text-left'</span>&gt;
                                {<span class="hljs-string">`©<span class="hljs-subst">${<span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().getFullYear()}</span>`</span>}{<span class="hljs-string">' '</span>}
                                &lt;a href=<span class="hljs-string">'#'</span> className=<span class="hljs-string">'text-primary'</span>&gt;
                                    TanStack Start
                                &lt;/a&gt;
                                , Made <span class="hljs-keyword">for</span> better web design
                            &lt;/p&gt;
                            &lt;div className=<span class="hljs-string">'flex items-center gap-5'</span>&gt;
                                &lt;a href=<span class="hljs-string">'#'</span>&gt;
                                    &lt;FacebookIcon className=<span class="hljs-string">'h-4 w-4'</span> /&gt;
                                &lt;/a&gt;
                                &lt;a href=<span class="hljs-string">'#'</span>&gt;
                                    &lt;InstagramIcon className=<span class="hljs-string">'h-4 w-4'</span> /&gt;
                                &lt;/a&gt;
                                &lt;a href=<span class="hljs-string">'#'</span>&gt;
                                    &lt;LinkedinIcon className=<span class="hljs-string">'h-4 w-4'</span> /&gt;
                                &lt;/a&gt;
                                &lt;a href=<span class="hljs-string">'#'</span>&gt;
                                    &lt;TwitterIcon className=<span class="hljs-string">'h-4 w-4'</span> /&gt;
                                &lt;/a&gt;
                            &lt;/div&gt;
                        &lt;/div&gt;
                    &lt;/footer&gt;
                &lt;/div&gt;
            &lt;/SidebarProvider&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>You now have a full layout for all <code>/dashboard/*</code> routes.</p>
<p>Let's break down the key parts of our dashboard layout:</p>
<ul>
<li><p><strong>Sidebar structure:</strong> The <code>&lt;Sidebar&gt;</code> component wraps our navigation menu. Inside, we use <code>&lt;SidebarMenu&gt;</code> and <code>&lt;SidebarMenuItem&gt;</code> to create navigation links. Each menu item uses TanStack Router's <code>&lt;Link&gt;</code> component for type-safe navigation. We also have a header set up in the <code>&lt;SidebarProvider&gt;</code></p>
</li>
<li><p><strong>Dynamic breadcrumbs:</strong> The breadcrumb section uses <code>location.pathname</code> to split the current URL into segments, then maps over them to create breadcrumb links. The <code>isLast</code> check ensures the final breadcrumb renders as plain text rather than a link.</p>
</li>
<li><p><strong>Header actions</strong>: The header includes two dropdowns: <code>&lt;LanguageDropdown&gt;</code> for internationalization and <code>&lt;ProfileDropdown&gt;</code> for user account actions. These come from the <code>shadcn/studio</code> blocks we installed.</p>
</li>
<li><p><strong>Outlet component:</strong> The <code>&lt;Outlet /&gt;</code> component is where child routes (like <code>/dashboard</code>, <code>/dashboard/products</code>) will render. This makes our layout reusable across all dashboard pages. The layout uses Tailwind's utility classes for spacing, colors, and responsive behavior, making it easy to customize for your use case.</p>
</li>
</ul>
<p>For more details regarding the Sidebar component, you can <a target="_blank" href="https://ui.shadcn.com/docs/components/sidebar">refer to the official docs here</a>.</p>
<p>You now have a full layout for all <code>/dashboard/*</code> routes.</p>
<h3 id="heading-6-building-the-dashboard-home-page">6. Building the Dashboard Home Page</h3>
<p>Create <code>src/routes/dashboard/index.tsx</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { <span class="hljs-keyword">type</span> Item } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/shadcn-studio/blocks/datatable-transaction'</span>
<span class="hljs-keyword">import</span> { createFileRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tanstack/react-router'</span>

<span class="hljs-keyword">import</span> { Card } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/ui/card'</span>

<span class="hljs-keyword">import</span> SalesMetricsCard <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/shadcn-studio/blocks/chart-sales-metrics'</span>
<span class="hljs-keyword">import</span> TransactionDatatable <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/shadcn-studio/blocks/datatable-transaction'</span>
<span class="hljs-keyword">import</span> StatisticsCard <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/shadcn-studio/blocks/statistics-card-01'</span>
<span class="hljs-keyword">import</span> ProductInsightsCard <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/shadcn-studio/blocks/widget-product-insights'</span>
<span class="hljs-keyword">import</span> TotalEarningCard <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/shadcn-studio/blocks/widget-total-earning'</span>

<span class="hljs-keyword">import</span> {
    CalendarX2Icon,
    TriangleAlertIcon,
    TruckIcon
} <span class="hljs-keyword">from</span> <span class="hljs-string">'lucide-react'</span>

<span class="hljs-comment">// Statistics card data</span>
<span class="hljs-keyword">const</span> StatisticsCardData = [
    {
        icon: &lt;TruckIcon className=<span class="hljs-string">'h-4 w-4'</span> /&gt;,
        value: <span class="hljs-string">'42'</span>,
        title: <span class="hljs-string">'Shipped Orders'</span>,
        changePercentage: <span class="hljs-string">'+18.2%'</span>
    },
    {
        icon: &lt;TriangleAlertIcon className=<span class="hljs-string">'h-4 w-4'</span> /&gt;,
        value: <span class="hljs-string">'8'</span>,
        title: <span class="hljs-string">'Damaged Returns'</span>,
        changePercentage: <span class="hljs-string">'-8.7%'</span>
    },
    {
        icon: &lt;CalendarX2Icon className=<span class="hljs-string">'h-4 w-4'</span> /&gt;,
        value: <span class="hljs-string">'27'</span>,
        title: <span class="hljs-string">'Missed Delivery Slots'</span>,
        changePercentage: <span class="hljs-string">'+4.3%'</span>
    }
]

<span class="hljs-comment">// Earning data for Total Earning card</span>
<span class="hljs-keyword">const</span> earningData = [
    {
        img: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/blocks/dashboard-application/widgets/zipcar.png'</span>,
        platform: <span class="hljs-string">'Zipcar'</span>,
        technologies: <span class="hljs-string">'Vuejs &amp; HTML'</span>,
        earnings: <span class="hljs-string">'-$23,569.26'</span>,
        progressPercentage: <span class="hljs-number">75</span>
    },
    {
        img: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/blocks/dashboard-application/widgets/bitbank.png'</span>,
        platform: <span class="hljs-string">'Bitbank'</span>,
        technologies: <span class="hljs-string">'Figma &amp; React'</span>,
        earnings: <span class="hljs-string">'-$12,650.31'</span>,
        progressPercentage: <span class="hljs-number">25</span>
    }
]

<span class="hljs-comment">// Transaction table data</span>
<span class="hljs-keyword">const</span> transactionData: Item[] = [
    {
        id: <span class="hljs-string">'1'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-1.png'</span>,
        avatarFallback: <span class="hljs-string">'JA'</span>,
        name: <span class="hljs-string">'Jack Alfredo'</span>,
        amount: <span class="hljs-number">315.0</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'jack@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'2'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-2.png'</span>,
        avatarFallback: <span class="hljs-string">'MG'</span>,
        name: <span class="hljs-string">'Maria Gonzalez'</span>,
        amount: <span class="hljs-number">253.4</span>,
        status: <span class="hljs-string">'pending'</span>,
        email: <span class="hljs-string">'maria.g@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'3'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-3.png'</span>,
        avatarFallback: <span class="hljs-string">'JD'</span>,
        name: <span class="hljs-string">'John Doe'</span>,
        amount: <span class="hljs-number">852.0</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'john.doe@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'4'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-4.png'</span>,
        avatarFallback: <span class="hljs-string">'EC'</span>,
        name: <span class="hljs-string">'Emily Carter'</span>,
        amount: <span class="hljs-number">889.0</span>,
        status: <span class="hljs-string">'pending'</span>,
        email: <span class="hljs-string">'emily.carter@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'5'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-5.png'</span>,
        avatarFallback: <span class="hljs-string">'DL'</span>,
        name: <span class="hljs-string">'David Lee'</span>,
        amount: <span class="hljs-number">723.16</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'david.lee@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'6'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-6.png'</span>,
        avatarFallback: <span class="hljs-string">'SP'</span>,
        name: <span class="hljs-string">'Sophia Patel'</span>,
        amount: <span class="hljs-number">612.0</span>,
        status: <span class="hljs-string">'failed'</span>,
        email: <span class="hljs-string">'sophia.patel@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'7'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-7.png'</span>,
        avatarFallback: <span class="hljs-string">'RW'</span>,
        name: <span class="hljs-string">'Robert Wilson'</span>,
        amount: <span class="hljs-number">445.25</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'robert.wilson@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'8'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-8.png'</span>,
        avatarFallback: <span class="hljs-string">'LM'</span>,
        name: <span class="hljs-string">'Lisa Martinez'</span>,
        amount: <span class="hljs-number">297.8</span>,
        status: <span class="hljs-string">'processing'</span>,
        email: <span class="hljs-string">'lisa.martinez@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'9'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-9.png'</span>,
        avatarFallback: <span class="hljs-string">'MT'</span>,
        name: <span class="hljs-string">'Michael Thompson'</span>,
        amount: <span class="hljs-number">756.9</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'michael.thompson@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'10'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-10.png'</span>,
        avatarFallback: <span class="hljs-string">'AJ'</span>,
        name: <span class="hljs-string">'Amanda Johnson'</span>,
        amount: <span class="hljs-number">189.5</span>,
        status: <span class="hljs-string">'pending'</span>,
        email: <span class="hljs-string">'amanda.johnson@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'11'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-11.png'</span>,
        avatarFallback: <span class="hljs-string">'KB'</span>,
        name: <span class="hljs-string">'Kevin Brown'</span>,
        amount: <span class="hljs-number">1024.75</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'kevin.brown@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'12'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-12.png'</span>,
        avatarFallback: <span class="hljs-string">'SD'</span>,
        name: <span class="hljs-string">'Sarah Davis'</span>,
        amount: <span class="hljs-number">367.2</span>,
        status: <span class="hljs-string">'failed'</span>,
        email: <span class="hljs-string">'sarah.davis@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'13'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-13.png'</span>,
        avatarFallback: <span class="hljs-string">'CG'</span>,
        name: <span class="hljs-string">'Christopher Garcia'</span>,
        amount: <span class="hljs-number">598.45</span>,
        status: <span class="hljs-string">'processing'</span>,
        email: <span class="hljs-string">'christopher.garcia@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'14'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-14.png'</span>,
        avatarFallback: <span class="hljs-string">'JR'</span>,
        name: <span class="hljs-string">'Jennifer Rodriguez'</span>,
        amount: <span class="hljs-number">821.3</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'jennifer.rodriguez@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'15'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-15.png'</span>,
        avatarFallback: <span class="hljs-string">'DM'</span>,
        name: <span class="hljs-string">'Daniel Miller'</span>,
        amount: <span class="hljs-number">156.75</span>,
        status: <span class="hljs-string">'pending'</span>,
        email: <span class="hljs-string">'daniel.miller@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'16'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-16.png'</span>,
        avatarFallback: <span class="hljs-string">'NW'</span>,
        name: <span class="hljs-string">'Nicole White'</span>,
        amount: <span class="hljs-number">934.1</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'nicole.white@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'17'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-17.png'</span>,
        avatarFallback: <span class="hljs-string">'AL'</span>,
        name: <span class="hljs-string">'Anthony Lopez'</span>,
        amount: <span class="hljs-number">412.85</span>,
        status: <span class="hljs-string">'failed'</span>,
        email: <span class="hljs-string">'anthony.lopez@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'18'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-18.png'</span>,
        avatarFallback: <span class="hljs-string">'MH'</span>,
        name: <span class="hljs-string">'Michelle Harris'</span>,
        amount: <span class="hljs-number">675.5</span>,
        status: <span class="hljs-string">'processing'</span>,
        email: <span class="hljs-string">'michelle.harris@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'19'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-19.png'</span>,
        avatarFallback: <span class="hljs-string">'JC'</span>,
        name: <span class="hljs-string">'James Clark'</span>,
        amount: <span class="hljs-number">289.95</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'james.clark@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'20'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-20.png'</span>,
        avatarFallback: <span class="hljs-string">'RL'</span>,
        name: <span class="hljs-string">'Rachel Lewis'</span>,
        amount: <span class="hljs-number">1156.25</span>,
        status: <span class="hljs-string">'pending'</span>,
        email: <span class="hljs-string">'rachel.lewis@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'21'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-21.png'</span>,
        avatarFallback: <span class="hljs-string">'TY'</span>,
        name: <span class="hljs-string">'Thomas Young'</span>,
        amount: <span class="hljs-number">543.6</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'thomas.young@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'22'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-22.png'</span>,
        avatarFallback: <span class="hljs-string">'SB'</span>,
        name: <span class="hljs-string">'Stephanie Brown'</span>,
        amount: <span class="hljs-number">789.3</span>,
        status: <span class="hljs-string">'processing'</span>,
        email: <span class="hljs-string">'stephanie.brown@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'23'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-23.png'</span>,
        avatarFallback: <span class="hljs-string">'BM'</span>,
        name: <span class="hljs-string">'Brandon Moore'</span>,
        amount: <span class="hljs-number">425.75</span>,
        status: <span class="hljs-string">'failed'</span>,
        email: <span class="hljs-string">'brandon.moore@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'24'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-24.png'</span>,
        avatarFallback: <span class="hljs-string">'KT'</span>,
        name: <span class="hljs-string">'Kelly Taylor'</span>,
        amount: <span class="hljs-number">1203.5</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'kelly.taylor@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'25'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-25.png'</span>,
        avatarFallback: <span class="hljs-string">'MA'</span>,
        name: <span class="hljs-string">'Mark Anderson'</span>,
        amount: <span class="hljs-number">356.2</span>,
        status: <span class="hljs-string">'pending'</span>,
        email: <span class="hljs-string">'mark.anderson@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    }
]

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Route = createFileRoute(<span class="hljs-string">'/dashboard/'</span>)({
    component: RouteComponent,
})

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RouteComponent</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> (
        &lt;div className=<span class="hljs-string">'grid grid-cols-2 gap-6 lg:grid-cols-3'</span>&gt;
            {<span class="hljs-comment">/* Statistics Cards */</span>}
            &lt;div className=<span class="hljs-string">'col-span-full grid gap-6 sm:grid-cols-3 md:max-lg:grid-cols-1'</span>&gt;
                {StatisticsCardData.map(<span class="hljs-function">(<span class="hljs-params">card, index</span>) =&gt;</span> (
                    &lt;StatisticsCard
                        key={index}
                        icon={card.icon}
                        title={card.title}
                        value={card.value}
                        changePercentage={card.changePercentage}
                    /&gt;
                ))}
            &lt;/div&gt;

            &lt;div className=<span class="hljs-string">'grid gap-6 max-xl:col-span-full lg:max-xl:grid-cols-2'</span>&gt;
                {<span class="hljs-comment">/* Product Insights Card */</span>}
                &lt;ProductInsightsCard className=<span class="hljs-string">'justify-between gap-3 *:data-[slot=card-content]:space-y-5'</span> /&gt;

                {<span class="hljs-comment">/* Total Earning Card */</span>}
                &lt;TotalEarningCard
                    title=<span class="hljs-string">'Total Earning'</span>
                    earning={<span class="hljs-number">24650</span>}
                    trend=<span class="hljs-string">'up'</span>
                    percentage={<span class="hljs-number">10</span>}
                    comparisonText=<span class="hljs-string">'Compare to last year ($84,325)'</span>
                    earningData={earningData}
                    className=<span class="hljs-string">'justify-between gap-5 sm:min-w-0 *:data-[slot=card-content]:space-y-7'</span>
                /&gt;
            &lt;/div&gt;

            &lt;SalesMetricsCard className=<span class="hljs-string">'col-span-full xl:col-span-2 *:data-[slot=card-content]:space-y-6'</span> /&gt;
            &lt;Card className=<span class="hljs-string">'col-span-full w-full py-0'</span>&gt;
                &lt;TransactionDatatable data={transactionData} /&gt;
            &lt;/Card&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>Our dashboard homepage uses various shadcn-studio blocks like:</p>
<ul>
<li><p><strong>Statistics cards</strong> display KPIs (Shipped Orders, Damaged Returns, and so on) with trend indicators. Each card receives props for the icon, value, title, and percentage change, making them reusable for any metric.</p>
</li>
<li><p><strong>Chart components</strong> like <code>&lt;SalesMetricsCard&gt;</code> use <code>recharts</code> under the hood to visualize data. The styling comes from shadcn/ui's card component and Tailwind utilities.</p>
</li>
<li><p><strong>Transaction data table</strong> demonstrates TanStack Table integration. We pass an array of transaction objects, and the <code>&lt;TransactionDatatable&gt;</code> component handles rendering, sorting, and pagination. Notice how we use TypeScript's <code>Item[]</code> type for full type safety.</p>
</li>
</ul>
<p>If you now navigate to <code>/dashboard</code>, you should see an admin dashboard with KPI statistics, charts, a dashboard, and a transaction table. Here is what it would look like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764747793227/ca1c0e10-e295-45c4-8e3c-15702583c887.jpeg" alt="tanstack start dashboard demo" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>We have built this beautiful dashboard quickly by using the shadcn/studio’s pre-built blocks.</p>
<h3 id="heading-7-set-up-the-products-page">7. Set up the Products Page.</h3>
<p>Before building our products table, we need to install <strong>Zod</strong>, a TypeScript-first schema validation library. We'll use it to validate the data structure of requests to our server function.</p>
<h4 id="heading-why-zod">Why Zod?</h4>
<p>TanStack Start's server functions use Zod to ensure type-safe data transfer between client and server. When we request to fetch products, Zod validates that the request includes the correct types for <code>page</code>, <code>pageSize</code>, <code>sortBy</code>, and <code>filters</code>. This catches errors at runtime and provides excellent TypeScript inference.</p>
<p>Now, let’s set up the products page with a products table. But before that, let’s install the zod package dependency. Here is the command for it:</p>
<pre><code class="lang-bash">pnpm add zod
</code></pre>
<h4 id="heading-creating-mock-product-data">Creating Mock Product Data</h4>
<p>We will need to store our mock products’ data somewhere. For that, we will create a new file <code>data/products.ts</code> and paste the code below. This will help us mock the product data for our products table.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createServerFn } <span class="hljs-keyword">from</span> <span class="hljs-string">"@tanstack/react-start"</span>;
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">"zod"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> Product = {
    id: <span class="hljs-built_in">string</span>
    name: <span class="hljs-built_in">string</span>
    category: <span class="hljs-built_in">string</span>
    price: <span class="hljs-built_in">number</span>
    stock: <span class="hljs-built_in">number</span>
    status: <span class="hljs-string">'active'</span> | <span class="hljs-string">'draft'</span> | <span class="hljs-string">'archived'</span>
    image: <span class="hljs-built_in">string</span>
}

<span class="hljs-comment">// Define the type for the data parameter</span>
<span class="hljs-keyword">type</span> ProductQueryParams = {
    page: <span class="hljs-built_in">number</span>;
    pageSize: <span class="hljs-built_in">number</span>;
    sortBy?: <span class="hljs-built_in">string</span>;
    sortOrder?: <span class="hljs-string">"asc"</span> | <span class="hljs-string">"desc"</span>;
    filters?: {
        name?: <span class="hljs-built_in">string</span>;
        category?: <span class="hljs-built_in">string</span>;
        status?: <span class="hljs-built_in">string</span>;
    };
};

<span class="hljs-keyword">const</span> products: Product[] = [
    {
        id: <span class="hljs-string">'PROD-001'</span>,
        name: <span class="hljs-string">'Wireless Noise Cancelling Headphones'</span>,
        category: <span class="hljs-string">'Electronics'</span>,
        price: <span class="hljs-number">299.99</span>,
        stock: <span class="hljs-number">45</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-002'</span>,
        name: <span class="hljs-string">'Ergonomic Office Chair'</span>,
        category: <span class="hljs-string">'Furniture'</span>,
        price: <span class="hljs-number">199.50</span>,
        stock: <span class="hljs-number">12</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1592078615290-033ee584e267?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-003'</span>,
        name: <span class="hljs-string">'Mechanical Gaming Keyboard'</span>,
        category: <span class="hljs-string">'Electronics'</span>,
        price: <span class="hljs-number">129.99</span>,
        stock: <span class="hljs-number">0</span>,
        status: <span class="hljs-string">'archived'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1587829741301-dc798b91add1?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-004'</span>,
        name: <span class="hljs-string">'Smart Fitness Watch'</span>,
        category: <span class="hljs-string">'Wearables'</span>,
        price: <span class="hljs-number">149.00</span>,
        stock: <span class="hljs-number">89</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-005'</span>,
        name: <span class="hljs-string">'Minimalist Desk Lamp'</span>,
        category: <span class="hljs-string">'Lighting'</span>,
        price: <span class="hljs-number">45.00</span>,
        stock: <span class="hljs-number">23</span>,
        status: <span class="hljs-string">'draft'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1507473888900-52e1ad14723b?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-006'</span>,
        name: <span class="hljs-string">'Portable Bluetooth Speaker'</span>,
        category: <span class="hljs-string">'Electronics'</span>,
        price: <span class="hljs-number">79.99</span>,
        stock: <span class="hljs-number">150</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1608043152269-423dbba4e7e1?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-007'</span>,
        name: <span class="hljs-string">'Ceramic Coffee Mug Set'</span>,
        category: <span class="hljs-string">'Kitchen'</span>,
        price: <span class="hljs-number">24.99</span>,
        stock: <span class="hljs-number">200</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1514228742587-6b1558fcca3d?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-008'</span>,
        name: <span class="hljs-string">'Leather Messenger Bag'</span>,
        category: <span class="hljs-string">'Accessories'</span>,
        price: <span class="hljs-number">129.50</span>,
        stock: <span class="hljs-number">15</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1553062407-98eeb64c6a62?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-009'</span>,
        name: <span class="hljs-string">'Wireless Charging Pad'</span>,
        category: <span class="hljs-string">'Electronics'</span>,
        price: <span class="hljs-number">39.99</span>,
        stock: <span class="hljs-number">75</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1586816879360-004f5b0c51e3?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-010'</span>,
        name: <span class="hljs-string">'Succulent Plant Set'</span>,
        category: <span class="hljs-string">'Home &amp; Garden'</span>,
        price: <span class="hljs-number">29.99</span>,
        stock: <span class="hljs-number">30</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1485955900006-10f4d324d411?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-011'</span>,
        name: <span class="hljs-string">'Professional Chef Knife'</span>,
        category: <span class="hljs-string">'Kitchen'</span>,
        price: <span class="hljs-number">89.95</span>,
        stock: <span class="hljs-number">42</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1593618998160-e34014e67546?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-012'</span>,
        name: <span class="hljs-string">'Yoga Mat'</span>,
        category: <span class="hljs-string">'Fitness'</span>,
        price: <span class="hljs-number">35.00</span>,
        stock: <span class="hljs-number">100</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1601925260368-ae2f83cf8b7f?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-013'</span>,
        name: <span class="hljs-string">'Smart Thermostat'</span>,
        category: <span class="hljs-string">'Home Automation'</span>,
        price: <span class="hljs-number">199.00</span>,
        stock: <span class="hljs-number">0</span>,
        status: <span class="hljs-string">'archived'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1567789884554-0b844b597180?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-014'</span>,
        name: <span class="hljs-string">'Vintage Film Camera'</span>,
        category: <span class="hljs-string">'Photography'</span>,
        price: <span class="hljs-number">450.00</span>,
        stock: <span class="hljs-number">3</span>,
        status: <span class="hljs-string">'draft'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1526170375885-4d8ecf77b99f?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-015'</span>,
        name: <span class="hljs-string">'Cotton T-Shirt Pack'</span>,
        category: <span class="hljs-string">'Apparel'</span>,
        price: <span class="hljs-number">49.99</span>,
        stock: <span class="hljs-number">150</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-016'</span>,
        name: <span class="hljs-string">'Electric Toothbrush'</span>,
        category: <span class="hljs-string">'Personal Care'</span>,
        price: <span class="hljs-number">69.99</span>,
        stock: <span class="hljs-number">55</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1559656914-a30970c1affd?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-017'</span>,
        name: <span class="hljs-string">'Gaming Mouse'</span>,
        category: <span class="hljs-string">'Electronics'</span>,
        price: <span class="hljs-number">59.99</span>,
        stock: <span class="hljs-number">88</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1527864550417-7fd91fc51a46?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-018'</span>,
        name: <span class="hljs-string">'Essential Oil Diffuser'</span>,
        category: <span class="hljs-string">'Home &amp; Garden'</span>,
        price: <span class="hljs-number">34.50</span>,
        stock: <span class="hljs-number">25</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1602928321679-560bb453f190?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-019'</span>,
        name: <span class="hljs-string">'Running Shoes'</span>,
        category: <span class="hljs-string">'Footwear'</span>,
        price: <span class="hljs-number">119.99</span>,
        stock: <span class="hljs-number">60</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-020'</span>,
        name: <span class="hljs-string">'Digital Drawing Tablet'</span>,
        category: <span class="hljs-string">'Electronics'</span>,
        price: <span class="hljs-number">249.00</span>,
        stock: <span class="hljs-number">18</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1561525140-c2a4cc68e4bd?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-021'</span>,
        name: <span class="hljs-string">'Bamboo Cutting Board'</span>,
        category: <span class="hljs-string">'Kitchen'</span>,
        price: <span class="hljs-number">22.99</span>,
        stock: <span class="hljs-number">95</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1594385208974-2e75f8d7bb48?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-022'</span>,
        name: <span class="hljs-string">'Sunglasses'</span>,
        category: <span class="hljs-string">'Accessories'</span>,
        price: <span class="hljs-number">159.00</span>,
        stock: <span class="hljs-number">40</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1511499767150-a48a237f0083?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-023'</span>,
        name: <span class="hljs-string">'Water Bottle'</span>,
        category: <span class="hljs-string">'Fitness'</span>,
        price: <span class="hljs-number">19.99</span>,
        stock: <span class="hljs-number">300</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1602143407151-01114192003f?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-024'</span>,
        name: <span class="hljs-string">'Throw Pillow Set'</span>,
        category: <span class="hljs-string">'Home Decor'</span>,
        price: <span class="hljs-number">45.99</span>,
        stock: <span class="hljs-number">28</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1584100936595-c0654b55a2e6?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-025'</span>,
        name: <span class="hljs-string">'Wireless Earbuds'</span>,
        category: <span class="hljs-string">'Electronics'</span>,
        price: <span class="hljs-number">89.99</span>,
        stock: <span class="hljs-number">120</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1590658268037-6bf12165a8df?w=100&amp;q=80'</span>,
    }
]

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getProducts = createServerFn({ method: <span class="hljs-string">"GET"</span> })
    .inputValidator(
        z.object({
            page: z.number().default(<span class="hljs-number">0</span>),
            pageSize: z.number().default(<span class="hljs-number">10</span>),
            sortBy: z.string().optional(),
            sortOrder: z.enum([<span class="hljs-string">"asc"</span>, <span class="hljs-string">"desc"</span>]).optional(),
            filters: z
                .object({
                    name: z.string().optional(),
                    category: z.string().optional(),
                    status: z.string().optional(),
                })
                .optional(),
        })
    )
    .handler(<span class="hljs-keyword">async</span> ({ data }: { data: ProductQueryParams }) =&gt; {
        <span class="hljs-keyword">const</span> { page, pageSize, sortBy, sortOrder, filters } = data;

        <span class="hljs-comment">// Apply filters</span>
        <span class="hljs-keyword">let</span> filteredProducts = [...products];

        <span class="hljs-keyword">if</span> (filters) {
            <span class="hljs-keyword">if</span> (filters.name) {
                filteredProducts = filteredProducts.filter(<span class="hljs-function">(<span class="hljs-params">product</span>) =&gt;</span>
                    product.name.toLowerCase().includes(filters.name!.toLowerCase())
                );
            }

            <span class="hljs-keyword">if</span> (filters.category) {
                filteredProducts = filteredProducts.filter(
                    <span class="hljs-function">(<span class="hljs-params">product</span>) =&gt;</span>
                        product.category.toLowerCase() === filters.category!.toLowerCase()
                );
            }

            <span class="hljs-keyword">if</span> (filters.status) {
                filteredProducts = filteredProducts.filter(
                    <span class="hljs-function">(<span class="hljs-params">product</span>) =&gt;</span> product.status === filters.status
                );
            }
        }

        <span class="hljs-comment">// Apply sorting</span>
        <span class="hljs-keyword">if</span> (sortBy) {
            filteredProducts.sort(<span class="hljs-function">(<span class="hljs-params">a, b</span>) =&gt;</span> {
                <span class="hljs-keyword">const</span> aValue = a[sortBy <span class="hljs-keyword">as</span> keyof Product];
                <span class="hljs-keyword">const</span> bValue = b[sortBy <span class="hljs-keyword">as</span> keyof Product];

                <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> aValue === <span class="hljs-string">"string"</span> &amp;&amp; <span class="hljs-keyword">typeof</span> bValue === <span class="hljs-string">"string"</span>) {
                    <span class="hljs-keyword">return</span> sortOrder === <span class="hljs-string">"desc"</span>
                        ? bValue.localeCompare(aValue)
                        : aValue.localeCompare(bValue);
                }

                <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> aValue === <span class="hljs-string">"number"</span> &amp;&amp; <span class="hljs-keyword">typeof</span> bValue === <span class="hljs-string">"number"</span>) {
                    <span class="hljs-keyword">return</span> sortOrder === <span class="hljs-string">"desc"</span> ? bValue - aValue : aValue - bValue;
                }

                <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
            });
        }

        <span class="hljs-comment">// Calculate pagination</span>
        <span class="hljs-keyword">const</span> totalCount = filteredProducts.length;
        <span class="hljs-keyword">const</span> totalPages = <span class="hljs-built_in">Math</span>.ceil(totalCount / pageSize);
        <span class="hljs-keyword">const</span> paginatedProducts = filteredProducts.slice(
            page * pageSize,
            (page + <span class="hljs-number">1</span>) * pageSize
        );

        <span class="hljs-comment">// Simulate network delay</span>
        <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve</span>) =&gt;</span> <span class="hljs-built_in">setTimeout</span>(resolve, <span class="hljs-number">500</span>));

        <span class="hljs-keyword">return</span> {
            products: paginatedProducts,
            pagination: {
                page,
                pageSize,
                totalCount,
                totalPages,
            },
        };
    });
</code></pre>
<p>Let’s understand the server function and break down what's happening in <code>getProducts</code>:</p>
<ul>
<li><p><strong>Input validation</strong>: The <code>.inputValidator()</code> method uses a Zod schema to validate incoming requests. It ensures <code>page</code> and <code>pageSize</code> are numbers, <code>sortOrder</code> is either "asc" or "desc", and filters are optional strings.</p>
</li>
<li><p><strong>Filtering products</strong>: The function filters the products array based on the provided filters (name, category, status). This simulates what a real database query would do.</p>
</li>
<li><p><strong>Sorting</strong>: Products are sorted by the specified column (<code>sortBy</code>) in ascending or descending order (<code>sortOrder</code>).</p>
</li>
<li><p><strong>Pagination</strong>: We calculate which slice of products to return based on <code>page</code> and <code>pageSize</code>, along with metadata like <code>totalCount</code> and <code>totalPages</code>.</p>
</li>
</ul>
<h4 id="heading-create-the-products-table">Create the Products table:</h4>
<p>Once the data is done, let’s create a table in <code>/dashboard/products.tsx</code>. This table will use our mock product data and will provide multiple functions in the table, like search, sort, and filter. This table demonstrates the powerful combination of TanStack Query for data management and TanStack Table for rendering.</p>
<p>Paste the code below in the <code>products.tsx</code> file:</p>
<pre><code class="lang-bash">import { useQuery } from <span class="hljs-string">'@tanstack/react-query'</span>
import { createFileRoute } from <span class="hljs-string">'@tanstack/react-router'</span>
import {
    ColumnDef,
    ColumnFiltersState,
    flexRender,
    getCoreRowModel,
    getFilteredRowModel,
    getPaginationRowModel,
    getSortedRowModel,
    SortingState,
    useReactTable,
    VisibilityState,
} from <span class="hljs-string">'@tanstack/react-table'</span>
import {
    ArrowUpDown,
    ChevronDown,
    Filter,
    Loader2,
    MoreHorizontal,
    Plus,
    Search
} from <span class="hljs-string">'lucide-react'</span>
import { useState } from <span class="hljs-string">'react'</span>

import { Badge } from <span class="hljs-string">'@/components/ui/badge'</span>
import { Button } from <span class="hljs-string">'@/components/ui/button'</span>
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from <span class="hljs-string">'@/components/ui/card'</span>
import {
    DropdownMenu,
    DropdownMenuCheckboxItem,
    DropdownMenuContent,
    DropdownMenuItem,
    DropdownMenuLabel,
    DropdownMenuSeparator,
    DropdownMenuTrigger,
} from <span class="hljs-string">'@/components/ui/dropdown-menu'</span>
import { Input } from <span class="hljs-string">'@/components/ui/input'</span>
import {
    Table,
    TableBody,
    TableCell,
    TableHead,
    TableHeader,
    TableRow,
} from <span class="hljs-string">'@/components/ui/table'</span>
import { getProducts, <span class="hljs-built_in">type</span> Product } from <span class="hljs-string">'@/data/products'</span>

<span class="hljs-built_in">export</span> const Route = createFileRoute(<span class="hljs-string">'/dashboard/products'</span>)({
    component: ProductsPage,
})

<span class="hljs-built_in">export</span> const columns: ColumnDef&lt;Product&gt;[] = [
    {
        accessorKey: <span class="hljs-string">'name'</span>,
        header: ({ column }) =&gt; {
            <span class="hljs-built_in">return</span> (
                &lt;Button
                    variant=<span class="hljs-string">"ghost"</span>
                    onClick={() =&gt; column.toggleSorting(column.getIsSorted() === <span class="hljs-string">"asc"</span>)}
                &gt;
                    Product Name
                    &lt;ArrowUpDown className=<span class="hljs-string">"ml-2 h-4 w-4"</span> /&gt;
                &lt;/Button&gt;
            )
        },
        cell: ({ row }) =&gt; (
            &lt;div className=<span class="hljs-string">"flex items-center gap-3"</span>&gt;
                &lt;img
                    src={row.original.image}
                    alt={row.getValue(<span class="hljs-string">'name'</span>)}
                    className=<span class="hljs-string">"h-10 w-10 rounded-md object-cover"</span>
                /&gt;
                &lt;div className=<span class="hljs-string">"flex flex-col"</span>&gt;
                    &lt;span className=<span class="hljs-string">"font-medium"</span>&gt;{row.getValue(<span class="hljs-string">'name'</span>)}&lt;/span&gt;
                    &lt;span className=<span class="hljs-string">"text-xs text-muted-foreground"</span>&gt;{row.original.id}&lt;/span&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        ),
    },
    {
        accessorKey: <span class="hljs-string">'category'</span>,
        header: <span class="hljs-string">'Category'</span>,
        cell: ({ row }) =&gt; &lt;div&gt;{row.getValue(<span class="hljs-string">'category'</span>)}&lt;/div&gt;,
    },
    {
        accessorKey: <span class="hljs-string">'status'</span>,
        header: <span class="hljs-string">'Status'</span>,
        cell: ({ row }) =&gt; {
            const status = row.getValue(<span class="hljs-string">'status'</span>) as string
            <span class="hljs-built_in">return</span> (
                &lt;Badge variant={status === <span class="hljs-string">'active'</span> ? <span class="hljs-string">'default'</span> : status === <span class="hljs-string">'draft'</span> ? <span class="hljs-string">'secondary'</span> : <span class="hljs-string">'outline'</span>}&gt;
                    {status}
                &lt;/Badge&gt;
            )
        },
    },
    {
        accessorKey: <span class="hljs-string">'price'</span>,
        header: () =&gt; &lt;div className=<span class="hljs-string">"text-right"</span>&gt;Price&lt;/div&gt;,
        cell: ({ row }) =&gt; {
            const amount = parseFloat(row.getValue(<span class="hljs-string">'price'</span>))
            const formatted = new Intl.NumberFormat(<span class="hljs-string">'en-US'</span>, {
                style: <span class="hljs-string">'currency'</span>,
                currency: <span class="hljs-string">'USD'</span>,
            }).format(amount)

            <span class="hljs-built_in">return</span> &lt;div className=<span class="hljs-string">"text-right font-medium"</span>&gt;{formatted}&lt;/div&gt;
        },
    },
    {
        accessorKey: <span class="hljs-string">'stock'</span>,
        header: () =&gt; &lt;div className=<span class="hljs-string">"text-right"</span>&gt;Stock&lt;/div&gt;,
        cell: ({ row }) =&gt; {
            const stock = parseFloat(row.getValue(<span class="hljs-string">'stock'</span>))
            <span class="hljs-built_in">return</span> &lt;div className={`text-right <span class="hljs-variable">${stock === 0 ? 'text-red-500 font-medium' : ''}</span>`}&gt;{stock}&lt;/div&gt;
        },
    },
    {
        id: <span class="hljs-string">'actions'</span>,
        enableHiding: <span class="hljs-literal">false</span>,
        cell: ({ row }) =&gt; {
            const product = row.original

            <span class="hljs-built_in">return</span> (
                &lt;DropdownMenu&gt;
                    &lt;DropdownMenuTrigger asChild&gt;
                        &lt;Button variant=<span class="hljs-string">"ghost"</span> className=<span class="hljs-string">"h-8 w-8 p-0"</span>&gt;
                            &lt;span className=<span class="hljs-string">"sr-only"</span>&gt;Open menu&lt;/span&gt;
                            &lt;MoreHorizontal className=<span class="hljs-string">"h-4 w-4"</span> /&gt;
                        &lt;/Button&gt;
                    &lt;/DropdownMenuTrigger&gt;
                    &lt;DropdownMenuContent align=<span class="hljs-string">"end"</span>&gt;
                        &lt;DropdownMenuLabel&gt;Actions&lt;/DropdownMenuLabel&gt;
                        &lt;DropdownMenuItem
                            onClick={() =&gt; navigator.clipboard.writeText(product.id)}
                        &gt;
                            Copy Product ID
                        &lt;/DropdownMenuItem&gt;
                        &lt;DropdownMenuSeparator /&gt;
                        &lt;DropdownMenuItem&gt;Edit Product&lt;/DropdownMenuItem&gt;
                        &lt;DropdownMenuItem&gt;View Details&lt;/DropdownMenuItem&gt;
                    &lt;/DropdownMenuContent&gt;
                &lt;/DropdownMenu&gt;
            )
        },
    },
]

<span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">ProductsPage</span></span>() {
    const [sorting, setSorting] = useState&lt;SortingState&gt;([])
    const [columnFilters, setColumnFilters] = useState&lt;ColumnFiltersState&gt;([])
    const [columnVisibility, setColumnVisibility] = useState&lt;VisibilityState&gt;({})
    const [rowSelection, setRowSelection] = useState({})
    const [pagination, setPagination] = useState({
        pageIndex: 0,
        pageSize: 10,
    })

    const { data, isLoading } = useQuery({
        queryKey: [<span class="hljs-string">'products'</span>, pagination, sorting, columnFilters],
        queryFn: () =&gt; getProducts({
            data: {
                page: pagination.pageIndex,
                pageSize: pagination.pageSize,
                sortBy: sorting[0]?.id,
                sortOrder: sorting[0]?.desc ? <span class="hljs-string">'desc'</span> : <span class="hljs-string">'asc'</span>,
                filters: {
                    name: (columnFilters.find((f) =&gt; f.id === <span class="hljs-string">'name'</span>)?.value as string) || undefined,
                    status: (columnFilters.find((f) =&gt; f.id === <span class="hljs-string">'status'</span>)?.value as string) || undefined,
                }
            }
        }),
    })

    const products = data?.products || []
    const totalPages = data?.pagination.totalPages || 0
    const totalCount = data?.pagination.totalCount || 0

    const table = useReactTable({
        data: products,
        columns,
        pageCount: totalPages,
        manualPagination: <span class="hljs-literal">true</span>,
        manualSorting: <span class="hljs-literal">true</span>,
        manualFiltering: <span class="hljs-literal">true</span>,
        onSortingChange: setSorting,
        onColumnFiltersChange: setColumnFilters,
        getCoreRowModel: getCoreRowModel(),
        getPaginationRowModel: getPaginationRowModel(),
        getSortedRowModel: getSortedRowModel(),
        getFilteredRowModel: getFilteredRowModel(),
        onColumnVisibilityChange: setColumnVisibility,
        onRowSelectionChange: setRowSelection,
        onPaginationChange: setPagination,
        state: {
            sorting,
            columnFilters,
            columnVisibility,
            rowSelection,
            pagination,
        },
    })

    <span class="hljs-built_in">return</span> (
        &lt;div className=<span class="hljs-string">"w-full space-y-4"</span>&gt;
            &lt;div className=<span class="hljs-string">"flex items-center justify-between"</span>&gt;
                &lt;h2 className=<span class="hljs-string">"text-2xl font-bold tracking-tight"</span>&gt;Products&lt;/h2&gt;
                &lt;div className=<span class="hljs-string">"flex items-center gap-2"</span>&gt;
                    &lt;Button variant=<span class="hljs-string">"outline"</span> size=<span class="hljs-string">"sm"</span>&gt;
                        &lt;Filter className=<span class="hljs-string">"mr-2 h-4 w-4"</span> /&gt;
                        Filter
                    &lt;/Button&gt;
                    &lt;Button size=<span class="hljs-string">"sm"</span>&gt;
                        &lt;Plus className=<span class="hljs-string">"mr-2 h-4 w-4"</span> /&gt;
                        Add Product
                    &lt;/Button&gt;
                &lt;/div&gt;
            &lt;/div&gt;

            &lt;Card&gt;
                &lt;CardHeader&gt;
                    &lt;CardTitle&gt;Product Management&lt;/CardTitle&gt;
                    &lt;CardDescription&gt;
                        Manage your product catalog, track inventory, and update prices.
                    &lt;/CardDescription&gt;
                &lt;/CardHeader&gt;
                &lt;CardContent&gt;
                    &lt;div className=<span class="hljs-string">"flex items-center py-4 gap-2"</span>&gt;
                        &lt;div className=<span class="hljs-string">"relative flex-1"</span>&gt;
                            &lt;Search className=<span class="hljs-string">"absolute left-2 top-2.5 h-4 w-4 text-muted-foreground"</span> /&gt;
                            &lt;Input
                                placeholder=<span class="hljs-string">"Filter products..."</span>
                                value={(table.getColumn(<span class="hljs-string">"name"</span>)?.getFilterValue() as string) ?? <span class="hljs-string">""</span>}
                                onChange={(event) =&gt;
                                    table.getColumn(<span class="hljs-string">"name"</span>)?.setFilterValue(event.target.value)
                                }
                                className=<span class="hljs-string">"pl-8 max-w-sm"</span>
                            /&gt;
                        &lt;/div&gt;
                        &lt;DropdownMenu&gt;
                            &lt;DropdownMenuTrigger asChild&gt;
                                &lt;Button variant=<span class="hljs-string">"outline"</span> className=<span class="hljs-string">"ml-auto"</span>&gt;
                                    Columns &lt;ChevronDown className=<span class="hljs-string">"ml-2 h-4 w-4"</span> /&gt;
                                &lt;/Button&gt;
                            &lt;/DropdownMenuTrigger&gt;
                            &lt;DropdownMenuContent align=<span class="hljs-string">"end"</span>&gt;
                                {table
                                    .getAllColumns()
                                    .filter((column) =&gt; column.getCanHide())
                                    .map((column) =&gt; {
                                        <span class="hljs-built_in">return</span> (
                                            &lt;DropdownMenuCheckboxItem
                                                key={column.id}
                                                className=<span class="hljs-string">"capitalize"</span>
                                                checked={column.getIsVisible()}
                                                onCheckedChange={(value) =&gt;
                                                    column.toggleVisibility(!!value)
                                                }
                                            &gt;
                                                {column.id}
                                            &lt;/DropdownMenuCheckboxItem&gt;
                                        )
                                    })}
                            &lt;/DropdownMenuContent&gt;
                        &lt;/DropdownMenu&gt;
                    &lt;/div&gt;
                    &lt;div className=<span class="hljs-string">"rounded-md border"</span>&gt;
                        &lt;Table&gt;
                            &lt;TableHeader&gt;
                                {table.getHeaderGroups().map((headerGroup) =&gt; (
                                    &lt;TableRow key={headerGroup.id}&gt;
                                        {headerGroup.headers.map((header) =&gt; {
                                            <span class="hljs-built_in">return</span> (
                                                &lt;TableHead key={header.id}&gt;
                                                    {header.isPlaceholder
                                                        ? null
                                                        : flexRender(
                                                            header.column.columnDef.header,
                                                            header.getContext()
                                                        )}
                                                &lt;/TableHead&gt;
                                            )
                                        })}
                                    &lt;/TableRow&gt;
                                ))}
                            &lt;/TableHeader&gt;
                            &lt;TableBody&gt;
                                {isLoading ? (
                                    &lt;TableRow&gt;
                                        &lt;TableCell colSpan={columns.length} className=<span class="hljs-string">"h-24 text-center"</span>&gt;
                                            &lt;div className=<span class="hljs-string">"flex items-center justify-center gap-2"</span>&gt;
                                                &lt;Loader2 className=<span class="hljs-string">"h-6 w-6 animate-spin"</span> /&gt;
                                                &lt;span&gt;Loading products...&lt;/span&gt;
                                            &lt;/div&gt;
                                        &lt;/TableCell&gt;
                                    &lt;/TableRow&gt;
                                ) : table.getRowModel().rows?.length ? (
                                    table.getRowModel().rows.map((row) =&gt; (
                                        &lt;TableRow
                                            key={row.id}
                                            data-state={row.getIsSelected() &amp;&amp; <span class="hljs-string">"selected"</span>}
                                        &gt;
                                            {row.getVisibleCells().map((cell) =&gt; (
                                                &lt;TableCell key={cell.id}&gt;
                                                    {flexRender(
                                                        cell.column.columnDef.cell,
                                                        cell.getContext()
                                                    )}
                                                &lt;/TableCell&gt;
                                            ))}
                                        &lt;/TableRow&gt;
                                    ))
                                ) : (
                                    &lt;TableRow&gt;
                                        &lt;TableCell
                                            colSpan={columns.length}
                                            className=<span class="hljs-string">"h-24 text-center"</span>
                                        &gt;
                                            No results.
                                        &lt;/TableCell&gt;
                                    &lt;/TableRow&gt;
                                )}
                            &lt;/TableBody&gt;
                        &lt;/Table&gt;
                    &lt;/div&gt;
                    &lt;div className=<span class="hljs-string">"flex items-center justify-end space-x-2 py-4"</span>&gt;
                        &lt;div className=<span class="hljs-string">"flex-1 text-sm text-muted-foreground"</span>&gt;
                            {table.getFilteredSelectedRowModel().rows.length} of{<span class="hljs-string">" "</span>}
                            {totalCount} row(s) selected.
                        &lt;/div&gt;
                        &lt;div className=<span class="hljs-string">"space-x-2"</span>&gt;
                            &lt;Button
                                variant=<span class="hljs-string">"outline"</span>
                                size=<span class="hljs-string">"sm"</span>
                                onClick={() =&gt; table.previousPage()}
                                disabled={!table.getCanPreviousPage()}
                            &gt;
                                Previous
                            &lt;/Button&gt;
                            &lt;Button
                                variant=<span class="hljs-string">"outline"</span>
                                size=<span class="hljs-string">"sm"</span>
                                onClick={() =&gt; table.nextPage()}
                                disabled={!table.getCanNextPage()}
                            &gt;
                                Next
                            &lt;/Button&gt;
                        &lt;/div&gt;
                    &lt;/div&gt;
                &lt;/CardContent&gt;
            &lt;/Card&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>Now you can see the fully functional products page by navigating the <code>/products</code> where you can search and sort the products.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764748681745/6f73dc04-ac9a-4f75-a1ab-88ed1fc5c6f3.jpeg" alt="tanstack start dashboard demo" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h4 id="heading-how-do-tanstack-query-and-tanstack-table-work-in-the-products-table">How do TanStack Query and TanStack Table Work in the products table?</h4>
<p>Our products page uses TanStack Query for data fetching and TanStack Table for rendering.</p>
<p><code>useQuery</code> is a fundamental hook in TanStack Query for managing server state in web applications. It simplifies data fetching, caching, and synchronization.</p>
<p>The below code snippet below shows how we have used useQuery in our product table:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useQuery } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tanstack/react-query'</span>;

<span class="hljs-keyword">const</span> { data, isLoading } = useQuery({
    queryKey: [<span class="hljs-string">'products'</span>, pagination, sorting, columnFilters],
    queryFn: <span class="hljs-function">() =&gt;</span> getProducts({...})
}
</code></pre>
<p>The <code>useQuery</code> hook manages data fetching in our application. For more details, you can <a target="_blank" href="https://tanstack.com/query/latest">refer to the official docs here</a>.</p>
<p><strong>useReactTable:</strong></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useReactTable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tanstack/react-table'</span>

<span class="hljs-keyword">const</span> table = useReactTable({
    data: products,
    columns,
    manualPagination: <span class="hljs-literal">true</span>,
    manualSorting: <span class="hljs-literal">true</span>,
    manualFiltering: <span class="hljs-literal">true</span>,
})
</code></pre>
<p><strong>TanStack Table</strong> manages the UI state and rendering. By setting <code>manualPagination</code>, <code>manualSorting</code>, and <code>manualFiltering</code> to <code>true</code>, we tell the table that server-side logic handles these operations.</p>
<p>When users sort, filter, or paginate, the table updates its states, and React Query detects the state change in the <code>queryKey</code>. It refetches data from the server, and the table re-renders with fresh data.</p>
<p>This architecture is production-ready and scales to thousands of rows. You just need to replace the mock API endpoint with your real API endpoint.</p>
<h3 id="heading-8-settings-page">8. Settings Page</h3>
<p>Finally, let’s add a simple Settings page with a profile section and some basic notification preferences.</p>
<p>Below is the code for the Settings Page. You can paste it into <code>/dashboard/settings.tsx</code>:</p>
<pre><code class="lang-bash">import { Avatar, AvatarFallback, AvatarImage } from <span class="hljs-string">'@/components/ui/avatar'</span>
import { Button } from <span class="hljs-string">'@/components/ui/button'</span>
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from <span class="hljs-string">'@/components/ui/card'</span>
import { Checkbox } from <span class="hljs-string">"@/components/ui/checkbox"</span>
import { Input } from <span class="hljs-string">'@/components/ui/input'</span>
import { Separator } from <span class="hljs-string">'@/components/ui/separator'</span>
import { createFileRoute } from <span class="hljs-string">'@tanstack/react-router'</span>

<span class="hljs-built_in">export</span> const Route = createFileRoute(<span class="hljs-string">'/dashboard/settings'</span>)({
  component: SettingsPage,
})

<span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">SettingsPage</span></span>() {
  <span class="hljs-built_in">return</span> (
    &lt;div className=<span class="hljs-string">"space-y-6"</span>&gt;
      &lt;div&gt;
        &lt;h3 className=<span class="hljs-string">"text-lg font-medium"</span>&gt;Settings&lt;/h3&gt;
        &lt;p className=<span class="hljs-string">"text-sm text-muted-foreground"</span>&gt;
          Manage your account settings and <span class="hljs-built_in">set</span> e-mail preferences.
        &lt;/p&gt;
      &lt;/div&gt;
      &lt;Separator /&gt;

      &lt;div className=<span class="hljs-string">"grid gap-6"</span>&gt;
        &lt;Card&gt;
          &lt;CardHeader&gt;
            &lt;CardTitle&gt;Profile&lt;/CardTitle&gt;
            &lt;CardDescription&gt;
              This is how others will see you on the site.
            &lt;/CardDescription&gt;
          &lt;/CardHeader&gt;
          &lt;CardContent className=<span class="hljs-string">"space-y-4"</span>&gt;
            &lt;div className=<span class="hljs-string">"flex items-center gap-4"</span>&gt;
              &lt;Avatar className=<span class="hljs-string">"h-20 w-20"</span>&gt;
                &lt;AvatarImage src=<span class="hljs-string">"https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-1.png"</span> /&gt;
                &lt;AvatarFallback&gt;JD&lt;/AvatarFallback&gt;
              &lt;/Avatar&gt;
              &lt;Button variant=<span class="hljs-string">"outline"</span>&gt;Change Avatar&lt;/Button&gt;
            &lt;/div&gt;
            &lt;div className=<span class="hljs-string">"space-y-1"</span>&gt;
              &lt;label htmlFor=<span class="hljs-string">"username"</span> className=<span class="hljs-string">"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"</span>&gt;Username&lt;/label&gt;
              &lt;Input id=<span class="hljs-string">"username"</span> defaultValue=<span class="hljs-string">"jdoe"</span> /&gt;
            &lt;/div&gt;
            &lt;div className=<span class="hljs-string">"space-y-1"</span>&gt;
              &lt;label htmlFor=<span class="hljs-string">"email"</span> className=<span class="hljs-string">"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"</span>&gt;Email&lt;/label&gt;
              &lt;Input id=<span class="hljs-string">"email"</span> defaultValue=<span class="hljs-string">"john.doe@example.com"</span> /&gt;
            &lt;/div&gt;
            &lt;div className=<span class="hljs-string">"space-y-1"</span>&gt;
              &lt;label htmlFor=<span class="hljs-string">"bio"</span> className=<span class="hljs-string">"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"</span>&gt;Bio&lt;/label&gt;
              &lt;Input id=<span class="hljs-string">"bio"</span> placeholder=<span class="hljs-string">"Tell us a little bit about yourself"</span> /&gt;
            &lt;/div&gt;
          &lt;/CardContent&gt;
          &lt;CardFooter&gt;
            &lt;Button&gt;Save Changes&lt;/Button&gt;
          &lt;/CardFooter&gt;
        &lt;/Card&gt;

        &lt;Card&gt;
          &lt;CardHeader&gt;
            &lt;CardTitle&gt;Notifications&lt;/CardTitle&gt;
            &lt;CardDescription&gt;
              Configure how you receive notifications.
            &lt;/CardDescription&gt;
          &lt;/CardHeader&gt;
          &lt;CardContent className=<span class="hljs-string">"space-y-4"</span>&gt;
            &lt;div className=<span class="hljs-string">"flex items-center justify-between rounded-lg border p-4"</span>&gt;
              &lt;div className=<span class="hljs-string">"space-y-0.5"</span>&gt;
                &lt;label className=<span class="hljs-string">"text-base font-medium"</span>&gt;Communication emails&lt;/label&gt;
                &lt;p className=<span class="hljs-string">"text-sm text-muted-foreground"</span>&gt;
                  Receive emails about your account activity.
                &lt;/p&gt;
              &lt;/div&gt;
              {/* Toggle would go here, using a simple checkbox <span class="hljs-keyword">for</span> now */}
              &lt;Checkbox defaultChecked /&gt;
            &lt;/div&gt;
            &lt;div className=<span class="hljs-string">"flex items-center justify-between rounded-lg border p-4"</span>&gt;
              &lt;div className=<span class="hljs-string">"space-y-0.5"</span>&gt;
                &lt;label className=<span class="hljs-string">"text-base font-medium"</span>&gt;Marketing emails&lt;/label&gt;
                &lt;p className=<span class="hljs-string">"text-sm text-muted-foreground"</span>&gt;
                  Receive emails about new products, features, and more.
                &lt;/p&gt;
              &lt;/div&gt;
              &lt;Checkbox /&gt;
            &lt;/div&gt;
          &lt;/CardContent&gt;
          &lt;CardFooter&gt;
            &lt;Button variant=<span class="hljs-string">"outline"</span>&gt;Update Preferences&lt;/Button&gt;
          &lt;/CardFooter&gt;
        &lt;/Card&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}
</code></pre>
<p>In this page, we have created two sections:</p>
<ol>
<li><p>Profile Section</p>
</li>
<li><p>Notification Section</p>
</li>
</ol>
<p>These two sections have been built using shadcn/ui components like Card, Footer, Checkbox, Avatar, Input, and so on.</p>
<p>At this point, we have:</p>
<ul>
<li><p>A dashboard layout with sidebar, header, breadcrumbs, and footer</p>
</li>
<li><p>A Dashboard page with charts, insights, and a transaction table</p>
</li>
<li><p>A Products page powered by:</p>
<ul>
<li><p>TanStack Start server functions</p>
</li>
<li><p>TanStack Query</p>
</li>
<li><p>TanStack Table</p>
</li>
</ul>
</li>
<li><p>A clean Settings page using shadcn/ui components</p>
</li>
</ul>
<h2 id="heading-live-demo-amp-source-code">Live Demo &amp; Source Code</h2>
<p>You can check out the full source code on GitHub here:</p>
<ul>
<li><p>GitHub Repository: <a target="_blank" href="https://github.com/themeselection/tanstack-dashboard-demo">https://github.com/themeselection/tanstack-dashboard-demo</a></p>
</li>
<li><p>Live Demo: <a target="_blank" href="https://tanstack-dashboard-demo.vercel.app/dashboard">https://tanstack-dashboard-demo.vercel.app/dashboard</a></p>
</li>
</ul>
<p>Feel free to clone, experiment, and extend it to fit your own application needs!</p>
<h2 id="heading-summary">Summary</h2>
<p>Congratulations! You've built a complete, production-ready admin dashboard using TanStack Start, TanStack Table, TanStack Query, Shadcn/ui, and shadcn/studio.</p>
<p>Throughout this tutorial, you’ve gained some hands-on experience in:</p>
<ul>
<li><p><strong>Full-stack application development with type safety</strong>: We’ve developed a full-stack application with TanStack Start's server functions with Zod validation to create type-safe APIs.</p>
</li>
<li><p><strong>Advanced data fetching</strong>: We’ve implemented TanStack Query for data fetching with automatic caching and background updates.</p>
</li>
<li><p><strong>Complex table interactions</strong>: We’ve built feature-rich data tables with TanStack Table, including server-side pagination, sorting, and filtering.</p>
</li>
<li><p><strong>Building UI quicker</strong>: We’ve leveraged shadcn/ui and shadcn/studio blocks to quickly build polished interfaces.</p>
</li>
<li><p><strong>Responsive layouts</strong>: And we’ve created adaptive designs that work seamlessly from mobile to desktop</p>
</li>
</ul>
<h3 id="heading-whats-next">What’s Next?</h3>
<p>Now that you have a solid foundation, consider implementing some or all of the below features if you want to work more on this:</p>
<ul>
<li><p><strong>Authentication</strong>: Add user authentication with Clerk, NextAuth, or Auth.js</p>
</li>
<li><p><strong>Real database</strong>: Replace mock data with Prisma + PostgreSQL or Drizzle + SQLite</p>
</li>
<li><p><strong>Form validation</strong>: Integrate React Hook Form with Zod for robust form handling</p>
</li>
<li><p><strong>Theming</strong>: Implement dark mode and custom color schemes using shadcn/ui's theming system</p>
</li>
<li><p><strong>API routes for CRUD</strong>: Add CRUD operations for products (create, update, delete)</p>
</li>
<li><p><strong>Internationalization:</strong> Make the dashboard compatible with multiple languages by integrating internationalization.</p>
</li>
</ul>
<p>We shipped a scalable and production-ready dashboard much faster than starting from scratch. Hope you enjoyed the process – and thanks for reading!</p>
<h3 id="heading-resources">Resources:</h3>
<ul>
<li><p><a target="_blank" href="https://tanstack.com/start">TanStack Start Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://tanstack.com/table">TanStack Table Docs</a></p>
</li>
<li><p><a target="_blank" href="https://tanstack.com/query">TanStack Query Docs</a></p>
</li>
<li><p><a target="_blank" href="https://shadcnstudio.com/components">Shadcn UI Components</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a CRUD App with TanStack Start and TanStackDB (with RxDB Integration) ]]>
                </title>
                <description>
                    <![CDATA[ TanStack Start is a new full-stack framework for React. It’s been growing in popularity ever since it reached the Release Candidate stage of its development in September, 2025. The Release Candidate stage is basically a version of software which is c... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-crud-app-with-tanstack-start-and-tanstackdb-with-rxdb-integration/</link>
                <guid isPermaLink="false">68ffdf4bd48b77fcbe0114ce</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tanstack ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Andrew Baisden ]]>
                </dc:creator>
                <pubDate>Mon, 27 Oct 2025 21:08:27 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761589878275/a36fe1d2-edf5-4339-b594-b1e55066485c.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>TanStack Start is a new full-stack framework for React. It’s been growing in popularity ever since it reached the Release Candidate stage of its development in September, 2025. The Release Candidate stage is basically a version of software which is considered to be almost complete, in a stable state, and ready for final public testing before its official launch.</p>
<p>TanStack Start has already started to emerge as a good alternative to other popular React frameworks like Next.js and Remix. The TanStack ecosystem is already quite popular with developers, and other well-known tools include:</p>
<ul>
<li><p><a target="_blank" href="https://tanstack.com/router/latest">TanStack Router</a>: Type-safe Routing for React and Solid applications</p>
</li>
<li><p><a target="_blank" href="https://tanstack.com/query/latest">TanStack Query</a>: Powerful asynchronous state management, server-state utilities and data fetching</p>
</li>
<li><p><a target="_blank" href="https://tanstack.com/form/latest">TanStack Form</a>: Headless UI for building performant and type-safe forms</p>
</li>
<li><p><a target="_blank" href="https://tanstack.com/db/latest">TanStackDB</a>: A reactive client store for building super-fast apps on sync</p>
</li>
</ul>
<p>In this tutorial, we’ll build a simple but powerful to-do list CRUD application using <a target="_blank" href="https://tanstack.com/start">TanStack Start</a>, <a target="_blank" href="https://tanstack.com/db">TanStackDB</a>, and <a target="_blank" href="https://rxdb.info/">RxDB</a>. You can see what the app looks like below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761220967371/3bd1f03c-e844-42bc-ac0f-b637e7cd6e61.png" alt="TanStack to do list CRUD App" class="image--center mx-auto" width="2526" height="1010" loading="lazy"></p>
<p>The tutorial will teach you how to:</p>
<ul>
<li><p>Create and persist data locally using RxDB</p>
</li>
<li><p>Create a TanStack Start project that uses TanStackDB for data storage</p>
</li>
<li><p>Build a full CRUD (Create, Read, Update, Delete) app</p>
</li>
</ul>
<p>At the end of this guide, we are also going to look at what makes TanStack Start different from other React frameworks like Next.js and Remix and how TanStackDB fits into this ever-growing ecosystem.</p>
<p>Let's get started.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-tanstack-start">What is TanStack Start?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-tanstack-db-with-rxdb-integration">What is TanStack DB (with RxDB Integration)?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-setting-up-our-project">Setting Up Our Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-creating-the-database-client">Creating the Database Client</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-understanding-local-persistence-with-rxdb">Understanding Local Persistence with RxDB</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-creating-a-todo-collection">Creating a Todo Collection</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-creating-our-crud-actions">Creating Our CRUD Actions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-creating-the-frontend-page">Creating the Frontend Page</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-does-tanstack-start-compare-to-nextjs-and-remix">How does TanStack Start Compare to Next.js and Remix?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-when-should-you-use-tanstack-start-nextjs-or-remix">When Should You Use TanStack Start, Next.js, or Remix?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Fortunately, not much is required – just the following:</p>
<ul>
<li><p>Node and npm installed</p>
</li>
<li><p>Code editor/IDE</p>
</li>
</ul>
<h2 id="heading-what-is-tanstack-start">What is TanStack Start?</h2>
<p>TanStack Start is a modern React-based meta-framework built by the developer Tanner Linsley (who’s famous for building the TanStack ecosystem).</p>
<p>TanStack Start is designed to be:</p>
<ul>
<li><p>Blazing fast, as it is powered by Vite, Bun, or other modern bundlers</p>
</li>
<li><p>Type-safe, because it is deeply integrated with TypeScript as well as TanStack Router</p>
</li>
<li><p>Lightweight, since there is no server-side rendering unless you want it</p>
</li>
<li><p>Full-stack ready, as it works with loaders, actions, and data mutations just like Remix</p>
</li>
</ul>
<p>If you’re already familiar with Next.js and Remix, you can think of TanStack Start as a more modular, transparent, and flexible way of building full-stack React apps.</p>
<h2 id="heading-what-is-tanstack-db-with-rxdb-integration">What is TanStack DB (with RxDB Integration)?</h2>
<p>TanStackDB is a reactive data management layer which sits between your user interface and data source. It’s not like a typical ORM (Object Relational Mapper). Instead, it gives you a unified abstraction layer for working with local-first data collections which are reactive.</p>
<p>So when you combine TanStackDB with RxDB, you get local database persistence which works by using IndexDB or SQLite and real-time reactivity. This gives you the ability to sync data to remote backends later like PostgreSQL, for example.</p>
<p>In this project, we’re going to use RxDB for local-first-storage that makes it behave like SQLite when it’s inside a browser.</p>
<h2 id="heading-setting-up-our-project">Setting Up Our Project</h2>
<p>Let’s start fresh. Find a location on your computer for creating this project and run these commands in your terminal to set it up:</p>
<pre><code class="lang-bash">npm create @tanstack/start@latest my-app
<span class="hljs-built_in">cd</span> my-app
npm install rxdb @tanstack/react-db @tanstack/rxdb-db-collection
mkdir -p src/db
touch src/db/actions.ts src/db/client.ts src/db/todoCollection.ts
</code></pre>
<p>This run script creates a TanStack Start project, installs dependencies for RxDB and TanStackDB, and creates the folders and files we need for our app.</p>
<p>At the end, we’re also going to replace the existing <code>index.tsx</code> page with our own CRUD app codebase, while also keeping the demo routes in navigation so you can still explore them.</p>
<h2 id="heading-creating-the-database-client">Creating the Database Client</h2>
<p>Up first is our <code>src/db/client.ts</code> file, so copy and paste the below code into the file:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { createRxDatabase, removeRxDatabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"rxdb"</span>;
<span class="hljs-keyword">import</span> { getRxStorageDexie } <span class="hljs-keyword">from</span> <span class="hljs-string">"rxdb/plugins/storage-dexie"</span>;

<span class="hljs-keyword">let</span> dbInstance: <span class="hljs-built_in">any</span> = <span class="hljs-literal">null</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">initDB</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-comment">// Only initialize in browser environment</span>
  <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> <span class="hljs-built_in">window</span> === <span class="hljs-string">"undefined"</span>) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"initDB: Not in browser, skipping"</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
  }

  <span class="hljs-keyword">if</span> (dbInstance) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"initDB: Returning existing instance"</span>);
    <span class="hljs-keyword">return</span> dbInstance;
  }

  <span class="hljs-keyword">try</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"initDB: Creating new database instance"</span>);
    <span class="hljs-keyword">const</span> storage = getRxStorageDexie();

    <span class="hljs-comment">// Always remove existing database in development</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">import</span>.meta.env.DEV) {
      <span class="hljs-keyword">try</span> {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"initDB: Removing existing database (dev mode)"</span>);
        <span class="hljs-keyword">await</span> removeRxDatabase(<span class="hljs-string">"appdb"</span>, storage);
      } <span class="hljs-keyword">catch</span> (e) {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"initDB: No existing database to remove"</span>);
      }
    }

    dbInstance = <span class="hljs-keyword">await</span> createRxDatabase({
      name: <span class="hljs-string">"appdb"</span>,
      storage,
      multiInstance: <span class="hljs-literal">false</span>,
      eventReduce: <span class="hljs-literal">true</span>,
    });

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"initDB: Database created successfully"</span>);
    <span class="hljs-keyword">return</span> dbInstance;
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"initDB: Failed to create database"</span>, error);
    <span class="hljs-keyword">throw</span> error;
  }
}

<span class="hljs-comment">// Cleanup for HMR</span>
<span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> <span class="hljs-built_in">window</span> !== <span class="hljs-string">"undefined"</span> &amp;&amp; <span class="hljs-keyword">import</span>.meta.hot) {
  <span class="hljs-keyword">import</span>.meta.hot.dispose(<span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"HMR: Disposing database"</span>);
    <span class="hljs-keyword">if</span> (dbInstance) {
      <span class="hljs-keyword">await</span> dbInstance.destroy();
      dbInstance = <span class="hljs-literal">null</span>;
    }
  });
}
</code></pre>
<p>This code uses RxDB to create a client-side database which is <code>appdb</code>. We use the function <code>getRxStorageDexie()</code> to provide IndexDB storage when used in browsers.</p>
<p>In dev mode, we can clear the DB on each reload, giving us a clean state. Server-side execution is protected by using the <code>window</code> check. HMR cleanup guarantees that the DB is reset properly when hot reloading.</p>
<h2 id="heading-understanding-local-persistence-with-rxdb">Understanding Local Persistence with RxDB</h2>
<p>Before we move to the next section, lets go over the concept of local persistence with RxDB. Our data is likely to disappear when the page is reloaded during development because RxDB used a browser-based database engine for persisting data locally. So we’ll be using the Dexie storage adapter which stores all of our apps data inside a browser’s IndexedDB.</p>
<p>So basically, this means that our todos actually don’t persist in the browser, even if we close and reopen the app – but there’s a way to get this working in our app.</p>
<p>In the <code>src/db/client.ts</code> file, there just happens to be a section of code that looks like this:</p>
<pre><code class="lang-typescript">    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">import</span>.meta.env.DEV) {
      <span class="hljs-keyword">try</span> {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"initDB: Removing existing database (dev mode)"</span>);
        <span class="hljs-keyword">await</span> removeRxDatabase(<span class="hljs-string">"appdb"</span>, storage);
      } <span class="hljs-keyword">catch</span> (e) {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"initDB: No existing database to remove"</span>);
      }
    }
</code></pre>
<p>This code is making sure that when we’re in development mode, our database is removed and then recreated every time the app is reloaded. This is quite useful, because when we’re actively developing and changing schemas, it can guarantee that old data is not going to conflict with new database structures.</p>
<p>The downside, though, is that todos will disappear every time the page is refreshed. This behaviour is expected while running our app locally in dev mode. If you want todos to persist between reloads, then all you have to do is comment out this code block:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Comment or remove this code block to persist data across reloads</span>
   <span class="hljs-keyword">if</span> (<span class="hljs-keyword">import</span>.meta.env.DEV) {
      <span class="hljs-keyword">try</span> {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"initDB: Removing existing database (dev mode)"</span>);
        <span class="hljs-keyword">await</span> removeRxDatabase(<span class="hljs-string">"appdb"</span>, storage);
      } <span class="hljs-keyword">catch</span> (e) {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"initDB: No existing database to remove"</span>);
      }
    }
</code></pre>
<p>After making this update, RxDB will now store your todos in IndexedDB and they will be automatically loaded whenever you revisit the app. You can even see this for yourself by opening your browser while the app is running and navigating to DevTools -&gt; Application -&gt; IndexedDB -&gt; appdb.</p>
<p>See the examples shown here:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761221020837/2a7fd37f-e638-4c6b-997f-ecde0aa789bb.png" alt="TanStack to do list app" class="image--center mx-auto" width="1114" height="746" loading="lazy"></p>
<p>Here you can see an example of what our app looks like with some tasks:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761221062751/431c4ac5-39d3-4adb-b197-bd4cc01a538e.png" alt="IndexedDB in the browser with tasks" class="image--center mx-auto" width="1814" height="1158" loading="lazy"></p>
<p>Here, you can see that our data stored inside IndexedDB in our browser.</p>
<p>The tasks should remain there until you have manually cleared the browser data.</p>
<h2 id="heading-creating-a-todo-collection">Creating a Todo Collection</h2>
<p>Now, let's work on our <code>src/db/todoCollection.ts</code> file. Copy and paste this code into the empty file in the codebase:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { initDB } <span class="hljs-keyword">from</span> <span class="hljs-string">"./client"</span>;

<span class="hljs-keyword">let</span> todoCollectionInstance: <span class="hljs-built_in">any</span> = <span class="hljs-literal">null</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createTodoCollection</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-comment">// Protect against server-side execution</span>
  <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> <span class="hljs-built_in">window</span> === <span class="hljs-string">"undefined"</span>) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"createTodoCollection: Not in browser, skipping"</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
  }

  <span class="hljs-keyword">if</span> (todoCollectionInstance) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"createTodoCollection: Returning existing collection"</span>);
    <span class="hljs-keyword">return</span> todoCollectionInstance;
  }

  <span class="hljs-keyword">try</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"createTodoCollection: Initializing database"</span>);
    <span class="hljs-keyword">const</span> db = <span class="hljs-keyword">await</span> initDB();

    <span class="hljs-keyword">if</span> (!db) {
      <span class="hljs-built_in">console</span>.error(
        <span class="hljs-string">"createTodoCollection: Database initialization returned null"</span>,
      );
      <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"createTodoCollection: Adding collections"</span>);
    <span class="hljs-keyword">if</span> (!db.todos) {
      <span class="hljs-keyword">await</span> db.addCollections({
        todos: {
          schema: {
            version: <span class="hljs-number">0</span>,
            primaryKey: <span class="hljs-string">"id"</span>,
            <span class="hljs-keyword">type</span>: <span class="hljs-string">"object"</span>,
            properties: {
              id: {
                <span class="hljs-keyword">type</span>: <span class="hljs-string">"string"</span>,
                maxLength: <span class="hljs-number">100</span>,
              },
              title: {
                <span class="hljs-keyword">type</span>: <span class="hljs-string">"string"</span>,
              },
              completed: {
                <span class="hljs-keyword">type</span>: <span class="hljs-string">"boolean"</span>,
              },
            },
            required: [<span class="hljs-string">"id"</span>, <span class="hljs-string">"title"</span>, <span class="hljs-string">"completed"</span>],
          },
        },
      });
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"createTodoCollection: Collections added successfully"</span>);
    }

    <span class="hljs-comment">// Return the RxDB collection directly</span>
    todoCollectionInstance = db.todos;

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"createTodoCollection: Collection created successfully"</span>);
    <span class="hljs-keyword">return</span> todoCollectionInstance;
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"createTodoCollection: Failed to create collection"</span>, error);
    <span class="hljs-keyword">throw</span> error;
  }
}
</code></pre>
<p>With this file, we define a <code>todos</code> collection schema which has an <code>id</code>, <code>title</code>, and <code>completed</code> fields. This schema is able to ensure that the structure and validation is correct, and we memoize the collection instance which prevents multiple DB connections from occurring. The code then returns a live RxDB collection which is ready for querying and mutation.</p>
<h2 id="heading-creating-our-crud-actions">Creating Our CRUD Actions</h2>
<p>It’s now time to work on our CRUD actions. These allow us to perform the usual updates/changes to the data in our todo list.</p>
<p>Open the <code>src/db/actions.ts</code> file and copy and paste this code into it:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { createTodoCollection } <span class="hljs-keyword">from</span> <span class="hljs-string">"./todoCollection"</span>;

<span class="hljs-keyword">let</span> collectionPromise: <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">any</span>&gt; | <span class="hljs-literal">null</span> = <span class="hljs-literal">null</span>;

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getCollection</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> <span class="hljs-built_in">window</span> === <span class="hljs-string">"undefined"</span>) {
    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
  }

  <span class="hljs-keyword">if</span> (!collectionPromise) {
    collectionPromise = createTodoCollection();
  }
  <span class="hljs-keyword">return</span> collectionPromise;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> TodoActions = {
  <span class="hljs-keyword">async</span> getAll() {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> collection = <span class="hljs-keyword">await</span> getCollection();
      <span class="hljs-keyword">if</span> (!collection) <span class="hljs-keyword">return</span> [];

      <span class="hljs-keyword">const</span> docs = <span class="hljs-keyword">await</span> collection.find().exec();
      <span class="hljs-keyword">return</span> docs.map(<span class="hljs-function">(<span class="hljs-params">doc: <span class="hljs-built_in">any</span></span>) =&gt;</span> ({
        id: doc.id,
        title: doc.title,
        completed: doc.completed,
      }));
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"TodoActions.getAll error:"</span>, error);
      <span class="hljs-keyword">throw</span> error;
    }
  },

  <span class="hljs-keyword">async</span> add(title: <span class="hljs-built_in">string</span>) {
    <span class="hljs-keyword">const</span> collection = <span class="hljs-keyword">await</span> getCollection();
    <span class="hljs-keyword">if</span> (!collection) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Collection not initialized"</span>);

    <span class="hljs-keyword">await</span> collection.insert({
      id: crypto.randomUUID(),
      title,
      completed: <span class="hljs-literal">false</span>,
    });
  },

  <span class="hljs-keyword">async</span> update(
    id: <span class="hljs-built_in">string</span>,
    changes: Partial&lt;{ title: <span class="hljs-built_in">string</span>; completed: <span class="hljs-built_in">boolean</span> }&gt;
  ) {
    <span class="hljs-keyword">const</span> collection = <span class="hljs-keyword">await</span> getCollection();
    <span class="hljs-keyword">if</span> (!collection) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Collection not initialized"</span>);

    <span class="hljs-keyword">const</span> doc = <span class="hljs-keyword">await</span> collection.findOne(id).exec();
    <span class="hljs-keyword">if</span> (doc) {
      <span class="hljs-keyword">const</span> patch: <span class="hljs-built_in">any</span> = {};
      <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> changes.title !== <span class="hljs-string">"undefined"</span>) patch.title = changes.title;
      <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> changes.completed !== <span class="hljs-string">"undefined"</span>)
        patch.completed = changes.completed;
      <span class="hljs-keyword">if</span> (<span class="hljs-built_in">Object</span>.keys(patch).length &gt; <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">await</span> doc.patch(patch);
      }
    }
  },

  <span class="hljs-keyword">async</span> toggle(id: <span class="hljs-built_in">string</span>) {
    <span class="hljs-keyword">const</span> collection = <span class="hljs-keyword">await</span> getCollection();
    <span class="hljs-keyword">if</span> (!collection) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Collection not initialized"</span>);

    <span class="hljs-keyword">const</span> doc = <span class="hljs-keyword">await</span> collection.findOne(id).exec();
    <span class="hljs-keyword">if</span> (doc) {
      <span class="hljs-keyword">await</span> doc.patch({ completed: !doc.completed });
    }
  },

  <span class="hljs-keyword">async</span> remove(id: <span class="hljs-built_in">string</span>) {
    <span class="hljs-keyword">const</span> collection = <span class="hljs-keyword">await</span> getCollection();
    <span class="hljs-keyword">if</span> (!collection) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Collection not initialized"</span>);

    <span class="hljs-keyword">const</span> doc = <span class="hljs-keyword">await</span> collection.findOne(id).exec();
    <span class="hljs-keyword">if</span> (doc) {
      <span class="hljs-keyword">await</span> doc.remove();
    }
  },
};
</code></pre>
<p>With this code, we use the <code>getCollection()</code> function to ensure that we only initialise the collection once. Each CRUD method (getAll, add, toggle, remove) interacts directly with RxDB and the methods use the native browser <code>crypto.randomUUID()</code> to generate a unique ID. We can now safely handle server-side rendering, as we skip DB access on the server using this strategy.</p>
<h2 id="heading-creating-the-frontend-page">Creating the Frontend Page</h2>
<p>All that remains is the frontend user interface, as we’ve written the bulk of the logic already. We’re going to replace the default <code>src/routes/index.tsx</code> file with our own CRUD UI, so just replace all of the code in that file with this code here:</p>
<pre><code class="lang-tsx">import * as React from "react";
import { createFileRoute } from "@tanstack/react-router";
import { TodoActions } from "../db/actions";

function Index() {
  const [todos, setTodos] = React.useState&lt;
    Array&lt;{ id: string; title: string; completed: boolean }&gt;
  &gt;([]);
  const [title, setTitle] = React.useState("");
  const [isLoading, setIsLoading] = React.useState(true);
  const [error, setError] = React.useState&lt;Error | null&gt;(null);
  const [editingId, setEditingId] = React.useState&lt;string | null&gt;(null);
  const [editingTitle, setEditingTitle] = React.useState("");

  React.useEffect(() =&gt; {
    let active = true;

    (async () =&gt; {
      try {
        console.log("Index: Loading todos");
        const data = await TodoActions.getAll();
        console.log("Index: Todos loaded", data);
        if (active) {
          setTodos(data);
          setIsLoading(false);
        }
      } catch (err) {
        console.error("Index: Failed to load todos:", err);
        if (active) {
          setError(err as Error);
          setIsLoading(false);
        }
      }
    })();

    return () =&gt; {
      active = false;
    };
  }, []);

  const handleAdd = async (e: React.FormEvent) =&gt; {
    e.preventDefault();
    if (title.trim()) {
      try {
        await TodoActions.add(title);
        setTodos(await TodoActions.getAll());
        setTitle("");
      } catch (err) {
        console.error("Failed to add todo:", err);
        setError(err as Error);
      }
    }
  };

  const handleToggle = async (id: string) =&gt; {
    try {
      await TodoActions.toggle(id);
      setTodos(await TodoActions.getAll());
    } catch (err) {
      console.error("Failed to toggle todo:", err);
    }
  };

  const handleRemove = async (id: string) =&gt; {
    try {
      await TodoActions.remove(id);
      setTodos(await TodoActions.getAll());
    } catch (err) {
      console.error("Failed to remove todo:", err);
    }
  };

  const startEdit = (todo: { id: string; title: string }) =&gt; {
    setEditingId(todo.id);
    setEditingTitle(todo.title);
  };

  const cancelEdit = () =&gt; {
    setEditingId(null);
    setEditingTitle("");
  };

  const saveEdit = async () =&gt; {
    if (!editingId) return;
    const newTitle = editingTitle.trim();
    if (!newTitle) return;
    try {
      await TodoActions.update(editingId, { title: newTitle });
      setTodos(await TodoActions.getAll());
      setEditingId(null);
      setEditingTitle("");
    } catch (err) {
      console.error("Failed to update todo:", err);
    }
  };

  if (isLoading) {
    return (
      &lt;main className="p-6 max-w-lg mx-auto"&gt;
        &lt;div className="text-center"&gt;Loading database...&lt;/div&gt;
      &lt;/main&gt;
    );
  }

  if (error) {
    return (
      &lt;main className="p-6 max-w-lg mx-auto"&gt;
        &lt;div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded"&gt;
          &lt;strong className="font-bold"&gt;Error: &lt;/strong&gt;
          &lt;span className="block sm:inline"&gt;{error.message}&lt;/span&gt;
          &lt;details className="mt-2"&gt;
            &lt;summary className="cursor-pointer"&gt;Show details&lt;/summary&gt;
            &lt;pre className="mt-2 text-xs overflow-auto"&gt;{error.stack}&lt;/pre&gt;
          &lt;/details&gt;
        &lt;/div&gt;
      &lt;/main&gt;
    );
  }

  return (
    &lt;main className="p-6 max-w-lg mx-auto"&gt;
      &lt;h1 className="text-2xl font-bold mb-4"&gt;TanStack CRUD (RxDB)&lt;/h1&gt;

      &lt;form onSubmit={handleAdd} className="flex gap-2 mb-4"&gt;
        &lt;input
          value={title}
          onChange={(e) =&gt; setTitle(e.target.value)}
          placeholder="Add a new task"
          className="border rounded px-3 py-2 flex-1"
        /&gt;
        &lt;button
          type="submit"
          className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
        &gt;
          Add
        &lt;/button&gt;
      &lt;/form&gt;

      &lt;ul&gt;
        {todos.length === 0 ? (
          &lt;li className="text-gray-500 text-center py-4"&gt;No todos yet&lt;/li&gt;
        ) : (
          todos.map((todo) =&gt; (
            &lt;li
              key={todo.id}
              className="flex justify-between items-center py-2 border-b"
            &gt;
              {editingId === todo.id ? (
                &lt;div className="flex w-full items-center gap-2"&gt;
                  &lt;input
                    value={editingTitle}
                    onChange={(e) =&gt; setEditingTitle(e.target.value)}
                    className="border rounded px-2 py-1 flex-1"
                  /&gt;
                  &lt;button
                    onClick={saveEdit}
                    className="bg-green-500 text-white px-3 py-1 rounded hover:bg-green-600"
                  &gt;
                    Save
                  &lt;/button&gt;
                  &lt;button
                    onClick={cancelEdit}
                    className="px-3 py-1 rounded border"
                  &gt;
                    Cancel
                  &lt;/button&gt;
                &lt;/div&gt;
              ) : (
                &lt;&gt;
                  &lt;span
                    onClick={() =&gt; handleToggle(todo.id)}
                    className={
                      todo.completed
                        ? "line-through cursor-pointer"
                        : "cursor-pointer"
                    }
                  &gt;
                    {todo.title}
                  &lt;/span&gt;
                  &lt;div className="flex items-center gap-3"&gt;
                    &lt;button
                      onClick={() =&gt; startEdit(todo)}
                      className="text-blue-500 hover:text-blue-700"
                    &gt;
                      Edit
                    &lt;/button&gt;
                    &lt;button
                      onClick={() =&gt; handleRemove(todo.id)}
                      className="text-red-500 hover:text-red-700"
                    &gt;
                      ✕
                    &lt;/button&gt;
                  &lt;/div&gt;
                &lt;/&gt;
              )}
            &lt;/li&gt;
          ))
        )}
      &lt;/ul&gt;
    &lt;/main&gt;
  );
}

export const Route = createFileRoute("/")({
  component: Index,
});
</code></pre>
<p>Our updated <code>index.tsx</code> file uses TanStack Router to define our root page, and we have React hooks to handle state, error handling, and the CRUD updates.</p>
<p>Our frontend is set up to show loading/error states for a much smoother UX, and each button triggers a corresponding <code>TodoActions</code> method. The result is that we have a fully reactive, local CRUD app.</p>
<p>That's all there is to it. To run the app, use the usual run command for a Vite application:</p>
<pre><code class="lang-bash">npm run dev
</code></pre>
<h2 id="heading-how-does-tanstack-start-compare-to-nextjs-and-remix">How Does TanStack Start Compare to Next.js and Remix?</h2>
<p>TanStack Start seems pretty impressive, right? But lets see how it compares to the other two big established frameworks, Next.js and Remix.</p>
<p>Next.js recently released <a target="_blank" href="https://nextjs.org/blog/next-16">version 16</a>, which brought some new improvements and features which you can read about. It’s without a doubt the most well known and used React framework available right now.</p>
<p>Remix also has a lot going for it, recently held its <a target="_blank" href="https://remix.run/blog/remix-jam-2025-recap">Remix Jam 2025 recap event</a> which you can also read and learn about.</p>
<p>TanStack Start, on the other hand, is built using the popular <a target="_blank" href="https://vite.dev/">Vite</a> build tool which it uses for its development, workflow, and production builds alongside TanStack Router and other libraries.</p>
<p>This is how all three compare when we put them side by side in a table:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Feature</td><td><strong>TanStack Start</strong></td><td><strong>Next.js</strong></td><td><strong>Remix</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Routing</td><td>TanStack Router</td><td>File-based</td><td>Nested routes</td></tr>
<tr>
<td>Type Safety</td><td>Deep TS integration</td><td>Partial</td><td>Full</td></tr>
<tr>
<td>Data Loading</td><td>Loaders/Actions</td><td>Server Components</td><td>Loaders/Actions</td></tr>
<tr>
<td>SSR Support</td><td>Optional</td><td>Built-in</td><td>Built-in</td></tr>
<tr>
<td>Bundler</td><td>Vite / Bun</td><td>Webpack / Turbopack</td><td>Remix Compiler</td></tr>
<tr>
<td>DX</td><td>Simple, minimal</td><td>Full-stack ecosystem</td><td>Full-stack with conventions</td></tr>
</tbody>
</table>
</div><p>As you can see, TanStack Start offers quite a lot of flexibility. It doesn’t force conventions like Next.js or Remix because of its design and it has just the right amount of structure for developers who want control and transparency in their projects. All three are great options, though.</p>
<h2 id="heading-when-should-you-use-tanstack-start-nextjs-or-remix">When Should You Use TanStack Start, Next.js, or Remix?</h2>
<p>Each of these frameworks has its advantages and disadvantages depending on your projects setup and priorities. We have to take into account the performance, flexibility, ecosystem as well as the developer experience.</p>
<p>With all of this in mind, it paints a clearer picture on when its best to use them.</p>
<h3 id="heading-when-to-use-tanstack-start">When to use TanStack Start</h3>
<p>If you want full control of your architecture without having to be locked into conventions, then TanStack Start is a great choice. It’s ideal if you value having transparency alongside the flexibility that comes from it.</p>
<p>You’ll find that its pretty useful especially in projects which need fine-grained control over routing, data fetching, and caching – without having to worry about the overhead of a large opinionated framework.</p>
<p>The integration between Vite and TanStack Router makes it a lightweight and blazing fast tool which can be great for greenfield projects and teams that want to have a modular setup.</p>
<h3 id="heading-when-to-use-nextjs">When to use Next.js</h3>
<p>Next.js is a great option when you need to have a production-ready scalability and extensive documentation, with a very large ecosystem. The framework has been a go to for startups as well as enterprises because of its tight integration with React Server Components, hosting with Vercel, and community driven packages.</p>
<p>So if SEO, SSR, or hybrid rendering are part of your team’s core needs, or if you want to shop something fast with a proven foundation, then Next.js is the safest and most mature way to go about doing it.</p>
<h3 id="heading-when-to-use-remixjs">When to use Remix.js</h3>
<p>Remix is a great choice when you want to have a strong focus on web fundamentals, progressive enhancement, and a reliable UX. It’s good for applications where you want to use a browser’s native capabilities such as forms, caching, and accessibility while also benefitting from a modern full-stack workflow.</p>
<p>It's also great for teams that want to have the simplicity of conventional routing and loaders while also remaining close to the original platform.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this article, we built a CRUD app from scratch using:</p>
<ul>
<li><p>TanStack Start for the app structure and routing</p>
</li>
<li><p>TanStackDB for the reactive data management</p>
</li>
<li><p>RxDB for an offline-first experience and local persistence</p>
</li>
</ul>
<p>You learned how to initialise a local database and collections as well as perform CRUD operations safely.</p>
<p>The TanStack ecosystem is quite powerful, and there are many tools available. They all fit really well together to give you a next-gen, local-first, reactive web development experience. TanStack Start is likely to become one of your favourite ways for building React applications and has a lot of potential for growth.</p>
<p>The official TanStack demos are still available in your nav on the homepage, and they are well worth checking out. Give the TanStack ecosystem a try. I think it could easily become your main tech stack.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
