Running Next.js navigation through react-query

In next.js pages router, every client navigation to routes with getServerSideProps/getStaticProps triggers a fetch request. What if we could run those client-side fetch requests through a client-side fetch library like react-query? I've created a next-query-glue library that allows you to do it. It brings a lot of control over data requests and greatly improves the user experience.

Next.js pages router default behavior

Nextjs pages router is pretty simple. It ensures that route data is fresh for every navigation. As a result, users have to wait for the getServerSideProps/getStaticProps response on every navigation. Navigation in Nextjs and most other metaframeworks is a blocking operation. Below is a simple demo website that imitates the behavior of the NextJS pages router. Navigate between pages to see how it works.

Nextjs pages router have some drawbacks:

  • No cache layer.
  • Route data can become stale, while staying on the same page.

Fetching route data through react-query

With react-query and next-query-glue, we get a stream of route data that updates constantly and automatically. In that article, I'll shortly describe some important properties of react-query. I highly recommend reading the react-query documentation.

  • staleTime: a time in milliseconds that a data should be considered fresh when attempting to load. Default value is 0, recommended 1 minute or more for fetching route data.
  • gcTime: a time in milliseconds after which, if a query is not called, data should be deleted from the cache. Default value is 5 minutes, recommended 5 minutes or more for fetching route data.

Those properties reveal how automatic invalidation works in react-query. It's not recommended to change them.

  • refetchOnWindowFocus (by default is true) - revalidates query data whenever the user makes tab or window of your website active.
  • refetchOnReconnect (by default is true) - revalidates query data once user regained a network connection.
  • refetchOnMount (by default is true) - revalidates query data once component calling useQuery is mounted.

Revalidation also can be triggered programmatically, through a simple API.

// Route data is now accessed through the useQuery hook. No more props drilling.
const { data: props, isLoading, isFetching } =  useQuery({
  queryKey,
  queryFn,
  staleTime: Infinity,
  gcTime: Infinity,
});

Thanks to next-query-glue, navigation now is a non-blocking operation. The page view updates instantly. While isLoading flag from useQuery is equal to true, the page's skeleton layout appears. The effect is similar to loading.jsx file from the app router, but with react-query, you manage the loading state at the component level, while the app router handles it at the route view level.

Optimistic navigation via placeholderData prop

Here's a quote from react-query documentation about placeholderData property

Example: An individual blog post query could pull "preview" data from a parent list of blog posts that only include title and a small snippet of the post body. You would not want to persist this partial data to the query result of the individual query, but it is useful for showing the content layout as quickly as possible while the actual query finishes to fetch the entire object.

Optimistic navigation is a good alternative to prefetching. When the "Lorem ipsum" article is opening for the first time, it feels that it opens instantly because of optimistic UI.

const { data: props, isLoading, isFetching } =  useQuery({
  queryKey,
  queryFn,
  staleTime: Infinity,
  gcTime: Infinity,
  placeholderData: {
    "title": "Lorem ipsum",
    "description": "Lorem ipsum dolor sit ...",
    "slug": "lorem-ipsum",
    "thumbnail": "/assets/image.jpg"
  },
});
// getServerSideProps response: 
[
  {
    "title": "Lorem ipsum",
    "description": "Lorem ipsum dolor sit ...",
    "slug": "lorem-ipsum",
    "thumbnail": "/assets/image.jpg"
  }
]

Every article object have a title, description, and thumbnail. We use that data to display a list of article cards, and we pass it as a placeholderData property in the useQuery hook. This explains why when the article is opened for the first time, its content displays a skeleton instead of actual data. The second and subsequent openings will utilize data from the cache.

Summary

React-query brings a lot of new client centric features to the router.

  • Client side SWR caching layer
  • Optimistic navigation (via optimistic UI)
  • Automatic and custom query invalidation
  • Query prefetching
  • Persistent Storage

Such a tight connection with react-query in next.js is possible via the `next-query-glue` library.