DEV Community

Cover image for React Query with Next.js ISR: Static websites with dynamic content
Charlie Joel
Charlie Joel

Posted on • Edited on • Originally published at dunders.dev

React Query with Next.js ISR: Static websites with dynamic content

What is stale-while-revalidate?

If there's any one technology I'll be keeping an eye on in the next few years, it's stale-while-revalidate: The ability to serve queries made in the past (stale) and then re-run those same queries once on the client side (revalidate). I (and many others) believe this new tech has the ability to revolutionise page speed forever. In the React space alone, there's already 2 serious contenders fulfilling similar roles: @tannerlinsley's react-query and @vercel's own swr package, with similar packages for other JavaScript frameworks. They both popped up at a similar time, too, with react-query's first commit back in September 2019 and swr starting just weeks later in late October 2019. While there are some minor differences between the two, they are aiming to solve the same problems.

What does SWR do?

The concept of stale-while-revalidate mainly aims to solve problems surrounded queries and caching, which can traditionally be clunky or fragile systems to custom-build.

Fetching and caching data

React Query and SWR do all the heavy lifting when it comes to data fetching: All you need to do is provide a query string and some options on when to refresh the data, and these packages will do the rest for you. Stale-while-revalidate packages use key strings to refer to queries, which can then be cached, refreshed in the background, or serve stale data while the new data is being fetched. All this comes with pretty much no configuration, which can save a lot of time.

const {
  isLoading,
  isError,
  data,
  error
} = useQuery('menus', fetchMenus) // 'menus' is the key

Thanks to the use of multiple keys on a single query, React Query and SWR can also handle pagination and any queries which use pointers straight out of the box, which can make paged requests much easier to deal with rather than creating custom code.

// page can be incremented and the query will update automatically
useQuery(
  ['products', page],
  () => fetchProducts(page)
)

Refetch on focus

One of the most handy features of stale-while-revalidate is refetch on focus: exactly what it sounds like, React Query will retry any queries specified once the user refocuses a window. This means if they've been on other sites, or just taken a break from the computer, the data will be refreshed the moment they return to your site - meaning complete data synchronisation with pretty much no effort.

Request failure/retry handling

If anything goes wrong with your request when using React Query or SWR, there's no need to worry: Errors and response failures are automatically handled. When something goes wrong, the error will be handled in the background and the query will be polled until it can get an 'OK' response. Until then, the stale data will be served so there's no need for a backup - just make sure you have a loading indicator so your users know what's going on.

Prefetching

For sites which are rendered on the server side, React Query can be set up to prefetch your page data using the same key system used on the page. There's a few unique functions to use for this - In React Query you can prefetchQuery:

// The results of this query will be cached like a normal query
const prefetchMenus = async () => {
   await queryClient.prefetchQuery('menus', fetchMenus)
 }

These queries made on the server side are then synchronised to the same queries which can be made on the page, so even if the data becomes stale it can be refreshed again on the client side.

Use cases for stale-while-revalidate

On its own, a package such as react-query or swr can offer some really handy use cases such as:

  • Making sure a feed is up-to-date when the user refocuses the window
  • Caching data fetched within a dialogue or modal, even if the modal is closed
  • Lazy loading - perform queries as and when required, serving stale data or placeholders until the request is fulfilled
  • Easily handle paginated sections or infinite scrolling
  • Improving fetching efficiency - since data is cached and assigned to a key in the background, this data is ready to be accessed anywhere on your application

Static vs dynamic websites

Traditionally, there have been two ways to serve websites: The first is static, where a site is exported into a plain HTML document only once before being served to all users, meaning the site content will be frozen from the point in time it was built. This means static sites are super fast to download and show content. One of the main drawbacks to static websites, however, is that the content they serve is frozen at the time the site is built. This is less than ideal for web apps, as one of the key features of the web is being up to date. This is the problem that dynamic websites aim to solve.

A dynamic website is one where a new version of each page is built for a particular user at the time they visit that page. This means the page content can be up-to-date and tailored to each user. This means data is completely up-to-date whenever a user visits the page. However, rendering the site on each request can be taxing for the server and increases loading times.

There's also the case that clients have an easier time editing dynamic websites thanks to their use of a content management system, although there are actually many static websites which also use a content management system, but these are less common. The main issue with using static websites alongside content management systems is they need to be rebuilt whenever any content changes, which might require either the client to manually rebuild the site through the console every time they make a change (good luck), or for webhooks to be added to the build process to detect changes and rebuild accordingly. Thankfully, Next.js has offered up a better way: Incremental Static Regeneration.

Incremental Static Regeneration with Next.js

You may have heard about a new feature of Next.js' build process - the name is a bit of a mouthful: Incremental Static Regeneration. Not the easiest to remember, but its benefits could leave a lasting impact on web performance in the 2020s - with 100/100 lighthouse performance scores every time and insanely fast page speeds

Here's how it works: Instead of exporting the website once and deploying that snapshot of the site, you provide a 'revalidate' timer in the getStaticProps function on your page. When you do 'npm run start' on your server, an image of the site will be exported at the current time. The interesting bit happens when the 'revalidate' timer you set up earlier runs out.

Once the timer gets down to 0, the server waits until another user visits a page on the site before rebuilding that page. This is where that 'stale-while-revalidate' concept comes back again: The user that revisits the page will get the stale content, but as soon as they refresh the page or another user comes along, they will receive the new, up-to-date version. In this way, ISR provides a deployment option that is somewhere between static and dynamic. The site will have the performance benefit of a static website, but the content will be up to date for most of the users.

This is also great for clients and webmasters: Instead of having to rebuild the whole site manually every time a change is made, you can just visit the page you edited and it will be rebuilt automatically.

But wait.

This still means that for users visiting a page that hasn't been visited in a while, the content will still be out of date. What can we do about that?

Using React Query with Next.js ISR to build static sites with dynamic content

Here comes the real beauty of using Next.js' ISR alongside React Query or a similar package. By asking React Query to re-run the same queries that were made when generating the site, this gap in fresh data can be immediately filled once the page is load client-side.

By setting up your queries to run once the page is loaded, and thanks to the keyed query caching system provided by React Query, this feature can be added in quite easily. It's also easy enough to refactor old Next.js projects to use this feature, so you can jump in and try it out with minimal effort.

Here's the performance score for a recent site I made. The only effort I really put into performance was to compress images and use optimal file types: everything else was handled by Next.js.

Lighthouse performance score

As you might be able to tell, I am really excited for this technology and any future developments of this idea: Perhaps in the future, a framework such as Next.js could handle all of this automatically, making super-fast dynamic websites the industry standard. For now, I encourage everyone working with headless systems to give this a try - extra performance is a great selling point for agencies and freelancers.

I'll be writing more about building headless systems on this blog in future, so keep an eye out if you're interested. Thanks for getting this far - if you disagree with anything I've said please let me know and I'll address it ASAP. Take care!

Top comments (0)