Dynamic Route in Next JS: Best Practices for Data Fetching and Rendering

For optimal performance in a dynamic route in Next JS, use Static Generation with Incremental Static Regeneration for cacheable content, hydrate with SWR or React Query for client-side updates, and leverage shallow routing for UI-only state changes to avoid unnecessary server fetches.

Implementing a dynamic route in Next JS requires careful consideration of when and where data fetching occurs to maximize performance. According to the Next.js source code, the framework supports multiple rendering strategies—from Static Generation to Edge SSR—that determine how dynamic segments like [id].tsx resolve and hydrate on the client.

Rendering Strategies for Dynamic Routes

Next.js matches a request to a dynamic route via the dynamicRoutes step in the router, as implemented in packages/next-routing/README.md. The server extracts parameters (such as id in [id].tsx) and makes them available through router.query or params.

Static Generation and Incremental Static Regeneration

For content that is publicly cacheable and does not need to be fresh on every request, use Static Generation (SSG) with getStaticProps and getStaticPaths. This generates HTML at build time and caches it at the edge.

// pages/posts/[id].tsx – SSG example
// https://github.com/vercel/next.js/blob/canary/examples/with-mongodb-mongoose/pages/%5Bid%5D/edit.tsx
export async function getStaticPaths() {
  const ids = await fetch('https://example.com/api/ids').then(r => r.json())
  return {
    paths: ids.map((id: string) => ({ params: { id } })),
    fallback: 'blocking',
  }
}

export async function getStaticProps({ params }: { params: { id: string } }) {
  const post = await fetch(`https://example.com/api/posts/${params.id}`).then(r => r.json())
  return {
    props: { post },
    revalidate: 60,
  }
}

The revalidate option enables Incremental Static Regeneration (ISR), allowing the page to regenerate in the background at most every 60 seconds. This balances performance with freshness for mostly-static data like blog posts or marketing pages.

Server-Side Rendering

For data that must be up-to-date on every request—such as authentication-protected pages or personalized content—use Server-Side Rendering (SSR) with getServerSideProps. This function runs on every request in packages/next/src/server/lib/router-server.ts, producing fresh HTML.

export async function getServerSideProps(context) {
  const session = await getSession(context.req)
  if (!session) {
    return { redirect: { destination: '/login', permanent: false } }
  }
  const data = await fetchUserData(session.userId)
  return { props: { data } }
}

Keep this function lightweight to avoid blocking the response pipeline. Heavy computation should be moved to a separate API route or Edge function.

Client-Side Performance Optimizations

After the initial server render, client-side data fetching strategies determine how the application stays responsive and up-to-date without sacrificing the benefits of static caching.

Hydration with SWR

Use SWR (or React Query) to fetch fresh data after the page loads. Seed the hook with the statically-generated payload using fallbackData to prevent a flash of loading state.

import useSWR from 'swr'
import { useRouter } from 'next/router'

const fetcher = (url: string) => fetch(url).then(r => r.json())

export default function Post({ post: initialPost }) {
  const { query } = useRouter()
  const { data: freshPost, error } = useSWR(
    query.id ? `/api/posts/${query.id}` : null,
    fetcher,
    { fallbackData: initialPost, revalidateOnFocus: false }
  )

  if (error) return <p>Failed to load.</p>
  if (!freshPost) return <p>Loading…</p>

  return (
    <article>
      <h1>{freshPost.title}</h1>
      <p>{freshPost.content}</p>
    </article>
  )
}

Setting revalidateOnFocus: false prevents unnecessary network calls when the user returns to the tab, though you can enable it for absolute freshness.

Shallow Routing for UI State

When only the URL query string changes—such as during pagination or filtering—use shallow routing to update the UI without re-running data-fetching methods.

import { useRouter } from 'next/router'

function Pagination({ page }) {
  const router = useRouter()

  const goToPage = (p: number) => {
    router.push(
      { pathname: router.pathname, query: { ...router.query, page: p } },
      undefined,
      { shallow: true }
    )
  }

  return <button onClick={() => goToPage(page + 1)}>Next</button>
}

With shallow: true, getStaticProps or getServerSideProps are not re-executed, providing instant UI updates while the client fetches new data independently.

Edge Runtime Considerations

For globally distributed users, run getServerSideProps in the Edge runtime to reduce latency. Configure this in packages/next/src/pages/_middleware.tsx patterns.

// pages/geo/[country].tsx
export const config = { runtime: 'edge' }

export async function getServerSideProps({ params }) {
  const res = await fetch(`https://geo.api/${params.country}`)
  const geo = await res.json()
  return { props: { geo } }
}

export default function Country({ geo }) {
  return (
    <>
      <h2>{geo.name}</h2>
      <p>Population: {geo.population}</p>
    </>
  )
}

The Edge runtime moves computation closer to the user but requires Edge-compatible APIs only—avoid Node-specific modules.

Key Implementation Files

Path Purpose
packages/next-routing/README.md Documents the dynamicRoutes matching step that extracts URL parameters.
packages/next/src/server/lib/router-server.ts Core server logic that resolves requests to dynamic pages and wires getStaticProps / getServerSideProps into the response pipeline.
examples/with-mongodb-mongoose/pages/[id]/edit.tsx Real-world implementation showing useRouter and client-side data fetching patterns.
test/unit/next-babel-loader-dev.test.ts (lines 252-279) Demonstrates how the bundler treats data-fetching methods during development to ensure production parity.
test/integration/server-side-dev-errors/pages/gsp.js Example page exporting getStaticProps with associated error handling paths.

Summary

  • Choose the right rendering mode: Use Static Generation with ISR for cacheable content, SSR for request-specific data, and Edge runtime for global low-latency responses.
  • Leverage shallow routing: Update query parameters without re-running getStaticProps or getServerSideProps to maintain instant UI responsiveness.
  • Hydrate intelligently: Use SWR with fallbackData to merge server-rendered props with client-side updates without loading flashes.
  • Keep server functions lightweight: Heavy computation belongs in API routes or background jobs, not in getServerSideProps.

Frequently Asked Questions

What is the difference between getStaticProps and getServerSideProps in a dynamic route?

getStaticProps runs at build time (or revalidation time for ISR) to generate static HTML that can be cached by CDNs, making it ideal for content that does not change frequently. getServerSideProps executes on every incoming request on the server, ensuring fresh data for personalized or authentication-protected pages, but it cannot be cached at the edge and increases server load.

When should I use shallow routing in Next.js dynamic routes?

Use shallow routing when you need to update the URL query string—such as for pagination, filters, or tabs—without triggering a full page reload or re-running data-fetching methods like getStaticProps. This keeps the client-side state synchronized with the URL while maintaining the performance benefits of your initial server render.

How does Incremental Static Regeneration improve performance for dynamic routes?

Incremental Static Regeneration (ISR) allows a statically generated page to be updated in the background at a specified interval (using the revalidate option) without requiring a full site rebuild. This means users receive a cached, instant HTML response while the server prepares an updated version for subsequent requests, balancing performance with content freshness.

Can I use the Edge runtime with getServerSideProps in dynamic routes?

Yes, you can configure getServerSideProps to run in the Edge runtime by exporting config = { runtime: 'edge' } from your page file. This executes the data-fetching logic at edge locations closer to the user, reducing latency, but you must ensure your code uses only Edge-compatible APIs and avoids Node.js-specific modules.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →