Introduction
Technology moves fast, and often we lose track of it. Most of us, we try to keep up with all the changes in our ecosystem, which may include experimenting with the latest libraries and frameworks, learning best practises etc. But, in order to experiment with those technologies, we need to get the opportunity, either including them in our pet projects or suggesting them in the place we work. Unfortunately, more often that we want, we regret those decisions because we didn't spend enough time evaluating if a particular technology was a good fit for our stack. Our impatient nature makes us regret those decisions, not to mention the technical debt that we leave behind.
The reason I'm grambling about this, is because I have seen a lot of videos suggesting the use of React Query with Next.js v14 lately, which I don't fully agree. Don't get me wrong, both tools are great in their own terms, but I don't feel that their combo is useful anymore, or at least, as good as people tend to say nowdays. Let me elaborate.
If you don't know the basics about those technologies, you can learn more about them here and here, but effectively react query is used to implement data fetching, caching and state management in next.js.
Before I proceed, I want to note that my claims have nothing to do with the pages router, and they concentrate only on the app router. If we were talking about the pages router, then my opinion would be completely different. In reality, it makes a lot of sense to use a data fetching library with the pages router. Using the pages router, we don't have access to caching, and if we need to implement something like that, we have to create an abstraction on top of it. Also, if you still use the pages router, chances are that you are not using the latest next.js and react versions, so you are also lacking features such us React Server Components(RSCs), server actions, and useOptimistic, useFormState, and useFormStatus hooks. These features can be used to implement optimistic updates, mutations, pagination and polling. So, react query can be used to provide coverage for those features as well.
Opinion
Caching is probably the number one selling point of react query. If you also combine it with the current development tools, you know how easy is to inspect a query, see when it was the last time that it was fetched, clear the cache, etc. Everything is very intuitive and easy to use.
Since version 13, the Next.js framework has it's own caching capabilities combined with React Server Components(RSCs). Everytime you fetch something, it is cached indefinitely, unless you opt-out caching, or set a time in your query that tells to the framework how often to revalidate cache. Basically, a page is statically rendered by default, and you can change it to dynamic when you update your page or cache configuration. For example, if you want to disable caching for all queries within a page or layout, you can set the dynamic flag to force-dynamic
, or revalidate to 0. Essentially, this matches the previous behavior of getServerSideProps
. But, if you need to disable caching for some queries and not all of them, then you need to add a few options in your fetch call. There are two options. You can either set revalidate to 0 or cache to no-store
in the fetch configuration. If you need to revalidate cache based on a certain interval, then you just need to set the a value in the revalidate option. Lastly. if you need to revalidate cache on-demand, next.js exposes the revalidateTag and revalidatePath utilities that run on the server. However, if you are interested to purge the cache on the client, you can use the router.refresh method, which is available from the router object.
So, since we explained how we handle caching for all those cases, there is one last question we need to answer.
Is that enough?
My answer is yes, more that enough for most web applications out there.
Honestly, most applications don't need anything more that. The majority of web applications are static, with only a few parts within them that are highly interactive(islands). So, using RSCs, we can fetch and build those static pages, and preload data to those islands of interactivity. Then, it's up to the client components to revalidate or fetch any additional data. Actually, let me change that. The client components don't need to fetch anything, they just need to re-run their parent RSCs, which will fetch fresh data, and reconcile those changes. The revalidation can be as simple as adding a query parameter on the existing url. Personally, I tend to use the refresh_at=[timestamp]
convention for the pages I need to revalidate. Keep in mind that react knows how to reconcile changes between server and client components without losing state, and that RSCs run on a request-response cycle. When we navigate in a dynamic page, a request is triggered, a server component runs and generates the RSC payload, which is then streamed to the client.
Moreover, a side-effect of this approach, is that we adopt a mindset where we run everything on the server(combined with server actions). Not only we can improve performance running our queries/mutations closest to our services, but we also improve the security of our application. Previously, when we wanted to access a microservice from the client, we had to set a JWT token in our request, which is often stored in cookies storage. But, in order the cookie to be visible and accessible via Javascript, we had to disable the httpOnly flag. That meant that our application was exposed on XSS attacks. But, if we were adopting a serve-only mindset, then we wouldn't have to consider any of these.
Summary
In short, these are the points you need to keep in mind:
Features such as data fetching, caching, mutations, pagination and optimistic updates were hard to implement in a Next.js application, but that's not the case anymore. That means that React Query is not as important as it used to be.
RSCs are mostly used to fetch data and pass down the output to client componets. Not only they allow to separate concerns, but they also improve performance and security.
RSCs are cached indefinitely by default, but there are also options to control caching on a page or request level.
RSCs are based on the request-response paradigm, which means that we can re-run them by adding a simple as a query parameter in the url. We can revalidate cache, implement pagination and polling using the same approach.
React knows to how to merge a server tree with a client tree without losing state.