vercel / next.js

The React Framework
https://nextjs.org
MIT License
125.73k stars 26.85k forks source link

Not able to revalidate server component on redirect. #56500

Closed okanji closed 2 months ago

okanji commented 1 year ago

Link to the code that reproduces this issue

https://github.com/okanji/revalidate-issue

Unable to revalidate a server rendered page that fetches db data via prisma when I re direct to it and the url is the same (i.e it is not dynamic and does not have any search params).

I need to manually re fresh the page to trigger the server component to re render and fetch the updated posts.

To Reproduce

See the sandbox: https://codesandbox.io/p/github/okanji/revalidate-issue/main?workspaceId=7c867f2b-0006-4b92-93b6-ae684cf1b42c

  1. Start the application, you will be automatically redirected to posts page.
  2. Follow the link to the create page.
  3. On the create post page which is a client rendered page, create a new post.
  4. You will automatically be redirected to the posts page which is server rendered.
  5. You will not see your new post unless you refresh the page.
  6. Refresh the page, now you will see the new post you created
  7. Repeat the steps above.

Current vs. Expected behavior

Current

I am using the following route segment config options in my server component.

import prisma from "@/lib/prisma";
import Link from "next/link";

// ** segment config options ** //
export const dynamic = "force-dynamic";
export const revalidate = 0;

export default async function Page() {
  const posts = await prisma.post.findMany();

  return (
    <div>
        <ul className="mt-8 space-y-4">
          {posts.map((post) => (
            <li
              key={post.id}
              className="p-4 bg-white shadow-lg rounded-md text-black"
            >
              {post.text}
            </li>
          ))}
        </ul>
    </div>
  );
}

This is in my client page where I navigate to the server page that shows all the posts. The relevant part is the router.push:

"use client";

import React, { useState } from "react";
import { useRouter } from "next/navigation";

function SimpleForm() {
  const router = useRouter();

  const [formData, setFormData] = useState({
    text: "",
  });
  const [loading, setLoading] = useState(false);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({
      ...formData,
      [name]: value,
    });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    await fetch("api/create-post", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ text: formData.text }),
    });
    setLoading(false);
    router.push("/server-page");
  };

  return (
    <div>
....

Expected

I expect that when I programmatically navigate to this page using router.push("/server-page"); from my client rendered page, the server page should re render and re fetch the data from the data base and show this new data (with my new post).

This is not the case. I manually need to refresh the page for it to update.

Verify canary release

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 22.2.0: Fri Nov 11 02:03:51 PST 2022; root:xnu-8792.61.2~4/RELEASE_ARM64_T6000
Binaries:
  Node: 18.13.0
  npm: 8.19.3
  Yarn: N/A
  pnpm: 7.29.1
Relevant Packages:
  next: 13.5.5-canary.2
  eslint-config-next: 13.5.4
  react: 18.2.0
  react-dom: 18.2.0
  typescript: 5.2.2
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

App Router, Data fetching (gS(S)P, getInitialProps), Routing (next/router, next/navigation, next/link)

Additional context

No response

okanji commented 1 year ago

Maybe this is a different approach I could use? Any help would be appreciated.

martinharyanto commented 1 year ago

I am not sure if this helps but usually i call router.refresh() after router.push(). But it is not 100% always works in dev environment (next dev)

okanji commented 1 year ago

Okay router.refresh() works! Is this the intended/ best way way to do things?

D34THNOTE commented 1 year ago

I also have this issue. I call revalidate(..) and then redirect(...) inside of a server action, which worked in localhost on version 13.4.13 but ceased working on localhost when I updated to version 13.5.3, even in a local production build

HOWEVER, this same code ran in my production deployment on Vercel still works - despite the build being on version 13.5.3 as well

To answer the router.refresh() question - no, it's been an extremely hacky solution since the App router got released. Due to App router's forced caching - which seems to be biting us here yet again, bug or otherwise - people have been struggling to provide the user with the latest data, this gets recommended sometimes but it is way off the "intended" or optimal solution

okanji commented 1 year ago

I see, hopefully we get a more solid solution in the near future!

schimi-dev commented 12 months ago

Hi, in my opinion revalidation works perfectly fine both for Server Actions and when handling form submissions manually.

When handling form submissions manually as @okanji's example you can use startTransition or useTransition to ensure that revalidation and navigation is happening in one elemental step regarding rendering in React. This will ensure that the new data is available for the ui when the navigation happens. Depending on your caching configuration it might be possible that your route handler/api route has to call revalidatePath or revalidateTag to clear any other cache than the Router Cache.

So, the Router Cache is cleared by router.refresh, the other cache layers have to be revalidated via the Server-side functions revalidatePath or revalidateTag as that can conceptually not happen on the client:

"use client";

import React, { useState, startTransition } from "react";
import { useRouter } from "next/navigation";

function SimpleForm() {
  const router = useRouter();

  const [formData, setFormData] = useState({
    text: "",
  });
  const [loading, setLoading] = useState(false);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({
      ...formData,
      [name]: value,
    });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    await fetch("api/create-post", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ text: formData.text }),
    });
    setLoading(false);
    startTransition(()=>{
        router.refresh();
        router.push("/server-page");
    })
  };

  return (
    <div>

When using Server Actions as described by @D34THNOTE revalidatePath or revalidateTag should be used over revalidate:

'use server'

import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'

export default async function submit() {
  const id = await addPost()
  revalidateTag('posts') // Update cached posts
  redirect(`/post/${id}`) // Navigate to new route
}

Here are some resources that will explain stuff in detail:

I think the documentation is quite good and comprehensive and from 13.5.x onwards the app router works really nice.

leerob commented 2 months ago

I believe this should be resolved on the latest version. Please try it out and let me know, and open a new issue if you are still seeing something. Thank you!

github-actions[bot] commented 2 months ago

This closed issue has been automatically locked because it had no new activity for 2 weeks. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.