DEV Community

loading...
Cover image for SWR + Dynamic Routes in Next.js

SWR + Dynamic Routes in Next.js

Jannik Baranczyk
Full Stack Developer · TypeScript, JAMstack, Coffee ✨
・3 min read

Hey folks!

If you recently worked with client side data fetching in Next.js, you probably heard of SWR. It comes with useSWR, a React hook that makes all the complicated stuff in client side data fetching (caching, revalidation, focus tracking etc.) easy as pie.

You can implement it with just a few lines of code:

// Import the hook
import useSWR from 'swr'

// Define a custom fetcher function
const fetcher = (url) => fetch(url).then((res) => res.json())

function Profile() {
  // Use the hook to fetch your data
  const { data, error } = useSWR('/api/user', fetcher)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>

  return <div>hello {data.name}!</div>
}
Enter fullscreen mode Exit fullscreen mode

Easy, right? Well, it definitely is when you try to fetch an endpoint with no query parameters, like /api/user. But when you try to pass a dynamic route parameter to your useSWR hook, things can get a little bit tricky. I recently spent some time figuring out a solution for this, so I thought I should share my solution.

Let's say we have a dynamic user route under /pages/user/[id].js, which should show a user profile based on the ID we pass as a route parameter.

The code to access that ID parameter would look like this:

// Import the useRouter hook from Next.js
import { useRouter } from 'next/router'

function Profile() {
  // Use the useRouter hook
  const router = useRouter()

  // Grab our ID parameter
  const { id } = router.query

  return <div>user id: {id}</div> 
}
Enter fullscreen mode Exit fullscreen mode

If you open that page with a random ID (http://localhost:3000/user/42 i.e), you should see the ID on the rendered page (user id: 42). Now, instead of just rendering that ID, let's fetch the user related to that ID from our API endpoint and render a profile page.

When I tried to do that, I thought I could just pass the ID parameter to the useSWR hook and voilá – a beautiful profile page. The code looked like that:

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

const fetcher = (url) => fetch(url).then((res) => res.json())

function Profile() {
  const router = useRouter()
  const { id } = router.query

  const { data, error } = useSWR(`/api/user/${id}`, fetcher)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>

  return <div>hello {data.name}!</div>
Enter fullscreen mode Exit fullscreen mode

But then the error messages came in – something obviously didn't work, my component just won't fetch the user. What happened here? When I had a look into the network tab, I noticed that the ID parameter wasn't passed to the fetch call – instead it said undefined. But why? The ID was clearly there, so what the heck happened here?

The answer is in the Next.js Docs:

Pages that are statically optimized by Automatic Static Optimization will be hydrated without their route parameters provided, i.e query will be an empty object ({}). After hydration, Next.js will trigger an update to your application to provide the route parameters in the query object.

Since I didn't use getServerSideProps or getStaticProps on that page, Next turned on Automatic Static Optimization for it – which means the dynamic parameters from router.query are not available until the hydration process has finished. Before, query is just an empty object – that's why the network tab said undefined.

So how can we tell useSWR to wait until our dynamic route parameter is ready?

TL;DR

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

const fetcher = (url) => fetch(url).then((res) => res.json())

function Profile() {
  const router = useRouter()
  const { id } = router.query

  // Use a ternary operator to only fetch the data when the ID isn't undefined
  const { data, error } = useSWR(id ? `/api/user/${id}` : null, fetcher)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>

  return <div>hello {data.name}!</div>
Enter fullscreen mode Exit fullscreen mode

This way our page now initially renders Loading..., and as soon as the hydration process has finished it fetches the user data and renders the profile.

I hope this little explanation could help you!

Discussion (9)

Collapse
simonswiss profile image
Simon Vrachliotis 🏄🏀💻😍

LOL I went through this exact discovery journey myself this morning, scratching my head as per why the router param was undefined..

I slowly but surely landed on this exact solution when finding this discussion:

github.com/vercel/next.js/discussi...

And of course, 2 minutes later, I land on your awesome post which is exactly what I was looking for all along 😅

Collapse
wh1zk1d profile image
Jannik Baranczyk Author

Thanks Simon! Haha, yeah, this discussion was also what helped me when I had that problem.

Big fan of your work with Tailwind Labs btw, keep up the good work! 🤙🏼

Collapse
brokenmold profile image
Jason Glass

You know, I actually just had a placeholder for my dynamic id in my code... but I would have been stumped on that here in about 10 minutes. So thanks for that advance heads up brother!

Collapse
hannah_thom_395725fc9364d profile image
Phalange

Thanks dude, awesome!
Was dealing with this issue all day long. Glad I found your article!

Collapse
wh1zk1d profile image
Jannik Baranczyk Author

You're welcome Phalange! Glad I could help you :) In which field of web dev are you working?

Collapse
room_js profile image
JavaScript Room

Good to know this, thank you for sharing! Next.js like any other framework has some hidden parts that are not obvious and have to be learnt. Good catch!

Collapse
wh1zk1d profile image
Jannik Baranczyk Author

You're welcome! :)

Collapse
inezabonte profile image
Ineza Bonté Grévy

Thank you mate. I'm using react-query facing the same issue. This helped.

Collapse
wh1zk1d profile image
Jannik Baranczyk Author

You're welcome my friend! Happy coding :)