Next.js Discord

Discord Forum

Server components, data fetching, revalidating - at a loss!

Unanswered
Velvet ant posted this in #help-forum
Open in Discord
Velvet antOP
I'm using the App router in v13 for the first time.

My page displays a list of components. They need to be editable, but will only occasionally be changed - so ideal for data-caching.

Following the documentation, my structure is:

* a server component <DataWrapper /> (see below) which fetches component data and passes it to
* a client component <UIWrapper /> which displays the component list

import 'server-only';
import UIWrapper from "./UIWrapper";

async function getData() {
    const res = await fetch(`${process.env.NEXT_URL}/api/component-info/getall`);

    if (!res.ok) {
        console.error('Failed to fetch data - error:\n', res);
        return [];
    }

    return res.json();
}


export default async function DataWrapper() {
    const data = await getData();

    return (
        <UIWrapper data={data} />
    )
}


When an edit is made in <UIWrapper />:

* the <UIWrapper /> fires a PUT to another api endpoint that updates the database
* the endpoint calls revalidatePath() on the getall route used in <DataWrapper />
* there's an artificial pause of 200ms before the enpoint returns
* on success, <UIWrapper /> calls router.refresh() to cause the page to update

All of which, as I understand from the docs, should mean that the data in <DataWrapper /> is refreshed and the updated info will show. in <UIWrapper />.

This does not work.

If I'm running in dev mode the endpoint updates (which I can see by calling it in a browser) but the UI doesn't. I can fix that by also adding a { next: { tags: [] } } to the fetch in <DataWrapper /> and calling revalidateTag alongside revalidatePath ...

... but with a build (so on Vercel) the only thing that works is adding a {cache: 'no-store' } to prevent the fetch being cached at all. Whilst that works, it's a huge waste of database time to constantly make calls when the data rarely changes.

So: am I missing something/going down the wrong path?!

31 Replies

Although we don't recommend calling route handlers from server components, it's confirmed that revalidatePath doesn't perform as expected.

https://github.com/vercel/next.js/issues/49778 - Closed, but still having problem in server actions
https://github.com/vercel/next.js/issues/49387 - Dynamic route doesn't work

A common workaround is to use revalidationTag instead of revalidatePath (as you have mentioned), or use res.revalidate in API Routes (Pages Router).
For example, our fourm is also using API Routes to revalidate pages.
https://github.com/rafaelalmeidatk/nextjs-forum/blob/main/apps/web/pages/api/revalidate-home.ts
Velvet antOP
OK, so two questions:
1. What is the recommended method with server components? I'm querying data from a (Vercel Postgres) db which I want to be able to cache and revalidate (it's likley that the data won't change week-to-week but I want to be able to change it and see the changes appear 'real-time' in the UI). I assume I could use an SQL query in my getData() function rather than a fetch - but presumably that would mean it's not cached by Next?
Velvet antOP
2. In testing to this point it appeared that to get a working version in the dev server I needed to both revalidateTag in the server component and revalidatePath for the route handler (otherwise the actual SQL query from the route handler was being cached). If I move the route handler into the Pages router rather than the App router does that solve that issue? Otherwise I need revalidatePath anyway ...
3. (I can't count) although using revalidateTag for the server component and revalidatePath for the route handler worked in the dev server, when built/deployed it wasn't casuing the page to be re-built
(I had something semi-working about 8 hours ago by calling revalidatePath on both the route handler (refresh the GET query) and the page path, but somehow in the last hour that's also stopped working and the server component appears to not be refreshing at all - frustrating!)
1. Yes, SQL queries won't be cached by Next.js
For now, It is possible with import { unstable_cache } from 'next/cache', but it's undocumented and we don't sure if it will be changed.

2. Correct, res.revalidate in Pages Router works as expected

3. Yep, it is quite frustrating. [Some people pointed it out](https://github.com/vercel/next.js/issues/49387#issuecomment-1567289235) in the issue
Velvet antOP
OK, so whilst I'm sure it's on the list of 'stuff to fix' it's obviously not an easy fix given it's been known for 3 months and not patched yet. I'm not keen on using { unstable_cache } (have enough headaches without adding experimental features!). It sounds as though an option (accepted not 'the recommened way') would be:
1. <DataWrapper /> makes a fetch to a route handler in the Pages router
2. When the <UIWrapper /> wants to edit entries it sends the request to a route handler in the Pages router
3. That handler can res.revalidate (the path to the page that is rendering <DataWrapper />? think it would be super helpful if the docs were clearer about whether 'path' refers to a path that's being fetched, path to a component, or path to a page that's being served!)
Would I also (and presumably first) need to revalidateTag for a tag on the getData function in <UIWrapper />?
And to access the Pages router is there any change needed, eg in next.config.js?
path refers to the path to a page, like /home or /about.
revalidateTag still works for revalidating tags, so you might need one if you want to revalidate a specific tag. However, since UIWrapper is a client component, it won't work.
Also, you don't need any changes in order to use Pages Router.
Velvet antOP
(just looking at the Pages Router version of the docs, much clearer there!)
Velvet antOP
So (apologies, I really want to get this straight in my head - I need to repeat the pattern several times and don't want to have re-write all of them!)

* assume that the DataWrapper and it's children are being rendered at /components inside my app
* does moving the getall route handler into Pages rather than App router prevent the actual SQL query result from being cached? If not, how do I prevent it? (guessing that having the router handler accept a POST or PUT method rather than GET would do it, but that feels icky)
* calling res.revalidate("/components") will mark that page path as outdated; assume still need to call router.refresh() in the UI once the 'update' route handler has returned in order for the UI to update?
* the important one - does res.revalidate cause the results of getData in the DataWrapper to be revalidated as well? If not, given you're saying I can't revalidate a tag (inside a route handler in the Pages router that's called from UIWrapper client component?) is there any way to cause that data to be revalidated? Or am I back to using cache: 'no-store again?
1. Pages Router doesn’t have cache
2. res.revalidate revalidates your page, including all the fetch in the server components of your page.

In addition, I want to clarify that SQL queries are not cached (if they not use fetch), but your page is cached. So revalidating the page can also update the result of SQL query
Velvet antOP
So (using the App Router for building the page, and Page Router only for any API) I could call an SQL query direcly in the DataWrapper server component; that component (or the page it's in) is cached; and res.revalidate from my (Page router) API will cause the page to revalidate - so effectively that achieves caching the data?
Yep
Velvet antOP
Great, there must be something in that lot that'll get me an answer!
Velvet antOP
One last question (sorry) — one of the issues that I've been struggling with has been ensuring the paths (or tags) are revalidated before the router.refresh() is called , since revalidateTag/revalidatePath in the App router don't return anything. From the docs it looks like res.revalidate is async-friendly so I can await it and then be certain that the data is invalidated by the time that UIWrapper gets a response?
Well I didn't test it yet so I can't give you an answer.
Technically it should be revalidated afterrouter.push because slate data must be revalidated before shipping it to the client.
I can only confirm that revalidatePath/tag will make the next request to be MISSING, which means it happens instantly.
so you can always ensure the page is revalidated after router.push, client can receive the latest data
Velvet antOP
(Ongoing) Have re-written/moved my API endpoints into Pages router. At the moment have made no further changes at all to the page. DataWrapper, UIWrapper etc.

Clear out the cache, start dev server, the getall endpoint returns data and the page (/component-info/ashp) shows it, so far so good.

If I then try to make an edit — say I'm deleting an item — my deleteone/[id] endpoint is called. Looks like:

import { sql } from '@vercel/postgres';

export default async function handler(request, response) {

    if (request.method !== 'DELETE') {
        return response.status(405).json({ message: 'Method Not Allowed' })
    }

    try {
        await sql`DELETE FROM ashps WHERE id = ${id};`

        await response.revalidate('/component-info/ashp');

        return response.status(200).json({ message: 'deleted' });

    } catch (error) {
        // ... error handling in here
    }
}


The delete is successful, but the endpoint then errors out at the revalidate with

Error: Invalid response 200
    ...
Error: Failed to revalidate /component-info/ashp: Invalid response 200
    ...


So: is there something else (obvious) that I should be doing/adding to the components in the App Router? Do I need to move the page to the Pages Router and look at using getStaticProps (at which point I guess I may as well just revert to the Pages Router entirely for this project)?
Remove status:
await res.revalidate('/')
res.json({ revalidated: true })
Velvet antOP
That's cutting out the second error, but still getting the first as below (with limited stack trace in case that helps)

Error: Invalid response 200
    at Object.revalidate (....../node_modules/next/dist/server/lib/router-server.js:122:23)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async Server.<anonymous> (./node_modules/next/dist/server/lib/server-ipc/index.js:56:30)
Velvet antOP
The docs for ISR only (?) appear in the Pages Router docs and concentrate on causing getStaticProps to run - which doesn't sound as though it's necessarily compatible with the App Router and server components
Velvet antOP
The solution: switch to Pages Router, use ISR, all good