DEV Community

Cover image for Reasons to Still Use Next.js from a TanStack Start Enjoyer
Dennis kinuthia
Dennis kinuthia

Posted on • Originally published at tigawanna.vip

Reasons to Still Use Next.js from a TanStack Start Enjoyer

Why You Might Still Pick Next.js Over TanStack Start

From a TanStack Start fanboy

TanStack Start is exciting. File-based routing, type-safe search params, and a stack that feels modern and, most importantly, lets you keep your SPA goodies and cool libraries like TanStack Query to maximize UX for your users and DX for yourself. We all know how heavy-handed mutations can be in Next.js, but with TanStack it all feels like a breeze. Lately, with server components quietly encroaching on Next.js territory, and a rising number of job postings asking for TanStack skills, the ecosystem is clearly growing.

That said, Next.js still wins in a few places that matter in production. This isn't a dunk on TanStack. It's a short list of reasons you might still choose Next.js, especially for mostly-static content sites.


1. Static Server State, Prebuild, and ISR

To be clear: this is not about TanStack Query client caching. This is about static server state: how Server Components and page output get stored, served, and regenerated on the server (Next's Full Route Cache / Data Cache style setup). Off Vercel that usually means wiring a KV (or similar) so the framework can actually persist that page-level server state. On Vercel, a lot of that plumbing is already there.

Next.js makes this easy. You declare revalidation once and the framework owns the rest:

// Next.js -- page-level ISR / static server state
export const revalidate = 3600 // regenerate at most once per hour

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await getPost(slug)
  return <Article post={post} />
}
Enter fullscreen mode Exit fullscreen mode

TanStack Start does have prebuild capabilities, but you still end up manually setting cache headers (or whatever your platform expects) to get proper edge/CDN behavior. Cloudflare made this friendlier with Workers Cache and solid stale-while-revalidate support (so the pieces exist), but Next's declarative revalidate, revalidatePath, and revalidateTag story is cleaner when SEO and freshness matter at scale. Less platform glue, more "set a number and ship."


2. Suspense-Aware Routing (and the Query-Param Flicker)

Next's data fetching and router integrate more cleanly with Suspense. TanStack Router leans on an external store for a lot of its state, and that store doesn't register with Suspense the way you'd hope.

A common pain point: a list driven by search params (filters, page, sort). Change a param, TanStack tracks it, your useSuspenseQuery re-suspends, and the Suspense boundary flickers.

useSuspenseQuery

In Next.js you'd wrap the update in useTransition so React keeps showing the old UI until the new data is ready. Same trick on TanStack Router / Start? Mostly a no-op: the navigate still lands, the query still suspends, and you still get the fallback flash.

Nextjs suspense transitions

Workarounds (neither ideal):

  1. Swap useSuspenseQuery for useQuery + keepPreviousData so you never hit a Suspense boundary on param changes. Old rows stay on screen; dim them with isFetching / isPlaceholderData.
  2. If you're on a fully suspenseful library (e.g. Relay), you're mostly stuck. Compose so controls (search, pagination) live outside the suspending region and only the data pane suspends. Filters stay mounted, but you still get flicker in the list where startTransition would have held previous data.

useQuery + keep previous data

Same patterns as the demos above, side by side:

// Pain: useSuspenseQuery + startTransition (no-op dampening)
function ProductsPage() {
  const { page, q } = Route.useSearch()
  const navigate = Route.useNavigate()
  const [isPending, startTransition] = useTransition()

  // Looks like the Next pattern. Does not hold previous UI here.
  // Router search updates aren't coordinated with Suspense the way
  // React state + use() are.
  function goToPage(nextPage: number) {
    startTransition(() => {
      navigate({ search: (prev) => ({ ...prev, page: nextPage }) })
    })
  }

  return (
    <>
      <Filters onPageChange={goToPage} />
      <div className={isPending ? "opacity-60" : ""}>
        <Suspense fallback={<ListSkeleton />}>
          <ProductResults page={page} q={q} />
        </Suspense>
      </div>
    </>
  )
}

function ProductResults({ page, q }: { page: number; q: string }) {
  // Changing page / q re-suspends → fallback flash
  // (even when navigate was wrapped in startTransition)
  const { data } = useSuspenseQuery(productsQueryOptions({ page, q }))
  return <ProductList products={data} />
}

// Workaround: useQuery + keepPreviousData (no Suspense flicker)
function ProductsPageKeepPrevious() {
  const { page, q } = Route.useSearch()
  const { data, isFetching, isPlaceholderData } = useQuery({
    ...productsQueryOptions({ page, q }),
    placeholderData: keepPreviousData,
  })

  return (
    <>
      <Filters />
      {/* dim via isFetching / isPlaceholderData */}
      <ProductList products={data} />
    </>
  )
}

// Next contrast: startTransition *does* hold the previous list
const [isPending, startTransition] = useTransition()

function selectColor(next: string) {
  startTransition(() => setColor(next)) // keeps old UI while Results suspends
}
Enter fullscreen mode Exit fullscreen mode

3. Image and Font Optimization

For mostly-static sites chasing Lighthouse / Core Web Vitals, Next's next/image and next/font are still a real advantage. TanStack is working on this space, but today the built-in story isn't as strong. If those scores are a hard requirement, Next is the safer bet.

import Image from 'next/image'
import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'], display: 'swap' })

export default function Hero() {
  return (
    <div className={inter.className}>
      <Image src={hero} alt="" width={1200} height={630} priority sizes="100vw" />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

next/image handles responsive sizes, lazy loading, and WebP/AVIF conversion out of the box. next/font subsets and self-hosts fonts to reduce CLS. In TanStack Start, you'd reach for Vite plugins like vite-imagetools or manual preload links. Both work, but require more configuration and ongoing maintenance.


4. React Canary Features (View Transitions) Built In

Another Next.js W: App Router ships on React canary, so features like the <ViewTransition> component are available without bolting on a separate experimental React install. Navigations already run inside React's startTransition, so the React-aware View Transition path can participate in the render lifecycle.

TanStack Start does support view transitions, but I've hit flashes of white before the animation starts. Part of that is the transition firing outside React's startTransition / Suspense coordination: blank paint, then the morph. It's also because the current TanStack implementation is the standard web View Transitions API: no React integration. Next gives you React's canary <ViewTransition> component, which actually participates in React's render lifecycle. Same feature name, very different depth of integration, and the React-aware path feels smoother out of the box.

nextjs-vs-tanstack-view traisitions

Tanstack version has white flashes that do not appear in the nextjs one with the same style of css
To remedy this I found that addinga display none to the old view helped

Before (flash / underlay linger)

[data-style="wipe"]::view-transition-old(root) {
  animation: none; /* old page stays under the wipe */
}

[data-style="wipe"]::view-transition-new(root) {
  animation-name: reveal-dark;
}
Enter fullscreen mode Exit fullscreen mode

The incoming page animates, but the outgoing snapshot stays painted underneath — so you see a blank/flash before the wipe finishes.

After (no flash)

[data-style="wipe"]::view-transition-old(root) {
  display: none; /* drop old snapshot immediately */
}

[data-style="wipe"]::view-transition-new(root) {
  animation-name: reveal-dark;
}
Enter fullscreen mode Exit fullscreen mode

Only the new view participates. Same pattern on Next with React’s class target — swap root for .canvas-vt.

5. Ecosystem Fit

This one's more personal (it might not move the needle for everyone), but it matters a lot if you live in that stack.

Payload CMS is a strong pick when you're already on Next.js. It ties into the App Router cleanly, gives you a highly customizable backend without building half of it yourself, and leans on Server Components to get a lot done. That's a plus TanStack Start simply doesn't have today.

Big caveat: the Payload team has said they're working on TanStack support. After Start gained Server Components, they've been taking it more seriously. They've talked about a TanStack integration landing before the next major Payload release. Until that ships, if you want Payload in your React app today, Next.js is the option.


Closing: When I'd Still Bite the Complexity and Pick TanStack Start

Most of these gaps will shrink as Start matures (including a real Payload integration). For static, crawlable content where ISR, Suspense-friendly nav, image/font SEO, canary React features, or Next-native CMS matter, Next still has the edge.

But I'm a Start fanboy for a reason. Classic fit: a light landing page attached to a heavy, auth-gated dashboard. No bot crawls it. First paint of 80ms vs 800ms barely matters when the user is about to wrestle charts and filters. Paying Next's full server-render + static-server-state tax for that UI is wasted compute and DX.

What you gain instead:

  • Full useQuery power: fine-grained fetches, optimistic updates, cache as source of truth, without the App Router's mutation → revalidate → stream-diff loop.
  • Typed search params in the router. Share a URL; coworkers land on the same filters. In Next you'd bolt on nuqs; in TanStack Router it's first-class.
  • Rendering escape hatches: data-only (hydrate from server-fetched data) or SSR off. Dump heavy charting into the browser instead of keeping server compute warm for dashboards no bot will score.

Doing that shape in Next is hard. Wiring Query into the App Router fights you toward server actions that invalidate and re-render the whole page. For a per-user, constantly moving dashboard, that's a quiet inefficiency: throw the page away, re-render on the server, ship a diff, for data the next person won't even share. Why run dynamic server caching if nothing is shared?

Adam Rackis breaks down how TanStack's RSC model differs: explicit, opt-in, not your primary data path (React Server Components in TanStack). Loaders + Query own data; RSCs keep expensive non-interactive trees off the client. In Next, RSC is the center of gravity. That gap is why auth-gated dashboards feel native in Start and upstream in Next.

So: static / SEO / CMS → Next. Thin public shell + thick private dashboard → Start, any day. Even after listing all those rough edges.

Top comments (0)