Have you ever done something similar to what the title suggests? Then you are probably doing it wrong, and this post was written for you. If you are uninterested in the explanation and only want the TLDR version, skip to the Takeaways section at the end.
However, I advise you to read the entire post because your understanding of Next.js will significantly increase once you finish it, and your future code base will thank you for this decision.
Data Fetching Strategies in Next.js
Multiple ways exist to achieve the same result, but not all are equally correct. Also, the final strategy will depend on your architecture. If you want to fetch data in a client component, you must do it differently than in a server component. Here are your options.
On The Server With The Fetch API
This is the first option that comes up in the Next.js docs, and there is a good reason for that because Next.js extends the fetch
API and automatically caches and memoizes its results.
Caching is so effective that if the route where you are calling your function doesn’t use any dynamic APIs like URL search params or cookies, the page will even be prerendered during next build
into a static page. This means those pages are turned into complete HTML files, and when someone visits them, they are served instantly. There is no rendering done at all.
Memoization, on the other hand, means that the same request isn’t sent twice to the same endpoint during dynamic rendering, no matter how many components need the same data.
On The Server With an ORM
However, there are cases when the fetch API is simply out of the question because you use an external library to get your data. An excellent example of this use case is when you are using an ORM. Instead of calling fetch(...)
, you are writing something like prisma.user.findUnique(...)
(in the case of Prisma) or something similar according to the model you want information from.
At this point, things get complicated as Next.js no longer caches and memoizes this request. But more on that later.
On The Client
You also have the option to fetch data on the client side, and there are multiple ways to do that. You can still use the fetch
API in a useEffect
(although this one is not recommended per the docs), or you can use SWR, React Query, or another React library of your choice. This is, let’s say, the traditional way of fetching data on the client, and those of you being used to SPAs are probably most familiar with this approach.
If you fetch data from your app and not from a remote backend, you must define an API route for that.
On The Client With Server Actions
The primary problem with this solution is that it is extremely developer-friendly, easy to use, and convenient. However, it is an antipattern that goes against the core idea of Server Actions, and for this reason, it isn’t documented at all in the Next.js docs.
But it works...
Then why is it a problem if I am using it?
You might ask. Well, there are other reasons if the above red flags weren’t enough. But first, I have to explain what Server Actions were invented for to see the bigger picture.
Server Actions Are For Mutating Data
To put it simply, this is the main reason. Server Actions were not invented to fetch data. This also shows in the way they are implemented. According to the docs:
Behind the scenes, actions use the POST method, and only this HTTP method can invoke them.
But because they look identical to other JavaScript functions — and as you will see, there are cases when they even behave like one — they can also return data, not just mutate it. This means using Server Actions for data fetching is entirely possible, even though it is an antipattern.
Why You Shouldn’t Fetch Data in Server Actions
The first reason is that the POST method is not for fetching data, but that hasn’t stopped anyone from using it as a getter so far. Here is a better reason, though.
A POST request cannot be memoized, meaning that if you are using it as a getter and it is called in multiple components and pages in your application, each occurrence of your function will trigger a new network request, even though each component needs exactly the same data. And since it is a POST request, the Cache-Control
header will be set to no-store, must-revalidate
, meaning it won’t be cached either.
React cache
I mentioned in the ORM fetching part that ORM requests are not memoized because Next.js only does that with native fetch
calls. However, there is also a way to make Next.js memoize custom functions by utilizing the cache
function from React. You simply have to wrap your function with another one like this:
This way, if getUser
is called in multiple components during dynamic rendering, only the first run will execute, and any subsequent calls will be returned from the cache.
The only problem is that the cache
function isn’t available in React yet. You have to install a canary version of React to use it. Let’s repeat this sentence.
You must use a canary version of React to use the cache
function.
I don’t know why this is only mentioned in the React docs, even though it is a crucial detail. Not even Next.js’ documentation mentions that this feature is only available if you are using a canary channel of React. You can install it like this, by the way:
Without this, you will probably face the following error:
You are not alone. Plenty of people struggle with the same problem.
Now that you have enabled caching and know how to use it, I will show you the proof of my statements above.
Examples
Here are some examples of the different data fetching patterns and their behavior.
I will use a non-cached and a cached dummy function for the demonstration.
Calling getUser
in a Server component
Let’s first call getUser
twice (non-cached versions) on the server.
This is what you will see in the logs.
The getUser
function is called twice.
Calling getUser
in a Client component
Let’s now call the same function but on the client.
In this case, you will also see 2 log statements.
The getUser
function is called twice.
If you run this locally, you will actually see 4 log statements, but that’s because React’s strict mode runs useEffect
functions twice in development mode.
Calling getUserCache
on the Server
Now, let’s do the same again, but I will call the cached version on the server this time.
Memoization activizes itself, and the logs will only show one function execution.
The getUserCache
function is called once.
Calling getUserCache on the Client
Now, for the final act, let’s call the cached function on the client.
The logs will show two function calls again.
The getUserCache
function is called twice.
This is normal because a POST request cannot be memoized. It’s true that two function calls aren’t the end of the world, but imagine if this user information was needed by dozens of components, and each would fire a new network request. Your application would quickly start to become laggy.
On the server, this is possible. You can call the same function in multiple places; only the first will be an actual function call. Because of request memoization, the remaining ones will be served from the in-memory cache.
The examples above lead to another question that begs to be answered.
Can I Call Server Actions on The Server?
TLDR: yes, you can. However, it will behave as a normal JavaScript function call.
I have also wondered about this question for a long time, even though I have been using it already, and it worked. But I didn’t know what happened under the hood until I tested it for this post.
Let’s call a Server Action on the server, and then let’s send a fetch request somewhere else.
As you can see, the Server Action does not appear in the logs as a network request, but the fetch
request does.
The fetch
request is visible in the logs.
To summarize the finding, you can call a Server Action on the server, and it will work. But in this case, the function simply behaves as a Data Access Object (DAO), a layer you put between your API and data store.
And since I hopefully already convinced you not to use Server Actions for data fetching, I also advise you to move that getter from your actions
folder and put it under dao
or something similar. I personally started following the pattern below:
- I keep functions that are getters or are called strictly on the backend in the
dao
. For example:/dao/users.ts
/dao/products.ts
- I am keeping functions that are used as Server Actions in
actions
. For example:/actions/users.ts
/actions/products.ts
There is also a way to enforce this architecture. You just have to install the server-only
package and import it at the top of your file like this:
This simple import ensures that if you accidentally want to use a DAO on the client, you will receive a build-time error.
The build will fail if you try to import a DAO on the client.
To turn functions inside a file into Server Actions, you must add the "use server";
directive at the top of the file. This can also be combined with the server-only
package, so now your actions file would start like this:
This setup will ensure that only actions can be accessed on the client side, not DAO functions. It will also ensure that you are using Server Actions in the intended way: in form actions, click handlers, useEffect, etc. Every other attempt will result in the same error above.
And, if you combine it with server-side data fetching and only use actions for data mutation, you are following every best practice, memoizing data fetching results and keeping your code clean.
One final topic remains unanswered: how do Server Actions work?
Are Server Actions API Endpoints Under The Hood?
Again, the short answer is yes.
The Next.js docs mention that "you should treat Server Actions as you would public-facing API endpoints" and authorize users accordingly, but I think this statement doesn’t emphasize well enough that they are API endpoints under the hood.
They even show up in the network logs as POST requests. And, for this reason, you can call them from Postman, Thunder Client, or anything else manually, and they will work. You only have to set a Next-Action
header to a string, which is probably a random ID used to identify which action to call if there are multiple ones used in the same component.
Remember this: if you are using Server Actions, you are invisibly adding POST API endpoints to your application that can be called manually. Authorize each request like you would any other API request.
Takeaways
This was a long run with plenty of lessons along the road. Let’s summarize everything we learned today and move it from the cache into the long-term memory.
Using Server Actions for Data Fetching
It works and it is convenient, but it is not what Server Actions were invented for, because it sends a POST request under the hood that cannot be cached and memoized, resulting in potential performance issues as the application grows.
Fetch data only on the server, or if you absolutely need client-side data fetching, create an API route for it.
Using The cache
Function from React
Next.js only extends the native fetch
API with memoization and caching. If you use something else for data fetching, like an ORM library, you can add the cache
function around it to enable memoization and caching.
However, cache
is currently only available in React's Canary channels, so you have to install that one to use it.
Calling Server Actions on The Server
It works and is simple but behaves like a regular JavaScript function call and doesn’t trigger a network request. To make the code base cleaner, it makes sense to move data fetchers into a DAO and only keep data mutating Server Actions in the actions
folder.
Server Actions Are API Endpoints
Under the hood, Server Actions are working API endpoints that anyone can call manually. They even show up in the network logs. Authorize every Server Action like you would public-facing API endpoints.