DEV Community

Cover image for Caching & Revalidate: ServerSide vs ClientSide
Rakshit Raj
Rakshit Raj

Posted on • Edited on

Caching & Revalidate: ServerSide vs ClientSide

A major feature which is often missed out by many web application especially dealing with data sharing and database like ecommerce websites, is data revalidation.

Like this one, where data is not always fetch from data source (database) but cached on the server to access latest revalidated data faster, also revalidating server component with a live UI update on client side as well as revalidating cache on server side for all clients.

data caching and revalidation demo

Data Revalidation

The process of reviewing, verifying, and ensuring the accuracy and completeness of data in a database or dataset. This is a crucial step in maintaining data quality and integrity. The revalidation process may involve checking for errors, inconsistencies, and outdated information.

Here outdated information is often referred to cached data on the servers. This is how things work under the hood though...

Nextjs caching and revalidation

For more information checkout latest Caching in Nextjs

In short, Caching just boost your page performance while maintaining the SEO and with the help of revalidation client always gets fresh data.

Practice Project

Let's make a mini demonstration project using these features.

Requirement

Step 1: Create an account on mockapi.io

Image description

Step 2: Create project

Image description

Step 3: Create Schema with new resource and generate data using the slider

Image description

You will be left with a url like this, replace the endpoint with your schema name

https://XXXXXXXXXXXXXXXXXXXXX.mockapi.io/api/product/:endpoint
Enter fullscreen mode Exit fullscreen mode

Create a .env.local file in the root directory

MOCKAPI = https://XXXXXXXXXXXXXXXXXXXXX.mockapi.io/api/product/:endpoint
Enter fullscreen mode Exit fullscreen mode

Server Side (App Directory)

Step 1: Initialize a Nextjs App using terminal

npx create-next-app server-demo
 Would you like to use TypeScript? ... Yes
 Would you like to use ESLint? ... Yes
 Would you like to use Tailwind CSS? ... Yes
 Would you like to use `src/` directory? ... Yes
 Would you like to use App Router? (recommended) ... Yes
? Would you like to customize the default import alias (@/*)? » No

cd server-demo
Enter fullscreen mode Exit fullscreen mode

Step 2 Inside src/app/page.tsx replace with this code below:

export default function Home() {
  return (
    <main className="grid max-w-3xl mx-auto p-4 gap-4">
      <h1 className="text-4xl font-extrabold max-lg:text-3xl">Homepage</h1>
      <div className="grid grid-cols-3 gap-3">
        <form
          action=""
          className="flex flex-col gap-2 p-1 rounded-lg bg-slate-400/10"
        >
          <input
            name="name"
            placeholder="Name"
            className="p-2 rounded-md outline-none"
            type="text"
          />
          <input
            name="image"
            placeholder="Image"
            className="p-2 rounded-md outline-none"
            type="text"
          />
          <button
            type="submit"
            className="bg-orange-600 font-extrabold uppercase text-white/70 p-2 rounded-md"
          >
            Submit
          </button>
        </form>
      </div>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Output

Image description

Step 3: Create a new file Card.tsx within src/app and paste this code below:

"use client";
import Image from "next/image";
import React from "react";

type Props = {
  props: {
    id: string;
    name: string;
    image: string;
    createdAt: string;
  };
};

const Card = ({ props }: Props) => {
  return (
    <div className="flex flex-col gap-1 p-1 rounded-lg bg-slate-400/10">
      <div className="relative overflow-hidden flex-1 rounded-md bg-slate-400/30">
        <Image
          src={props.image}
          alt={props.name}
          fill
          loader={() => props.image}
          className="object-cover"
        />
      </div>
      <span className="bg-zinc-800 font-extrabold uppercase text-white/70 p-2 rounded-md text-center">
        {props.name}
      </span>
    </div>
  );
};

export default Card;
Enter fullscreen mode Exit fullscreen mode

Now import Card.tsx into the page.tsx just below </form>

//</form>
//Card Component
<Card props={{
      id: "1",
      name: "Bellgium",
      image: "https://loremflickr.com/640/480/nature",
      createdAt: "2023-11-29T05:14:05.972Z",
}} />
//</div>
Enter fullscreen mode Exit fullscreen mode

Output

Image description

Step 4: Remember to make default function async in page.tsx

export default async function Home()
Enter fullscreen mode Exit fullscreen mode

Fetching data on server component with the mockapi endpoint

//export default async function Home() {
  const res = await fetch(process.env.MOCKAPI!, {
    next: {
      tags: ["place"],
      revalidate: 60 * 60,
    },
  });
  const data = await res.json();
  //return (
Enter fullscreen mode Exit fullscreen mode

Using fetched data below by wrapping Card component inside array map function

//</form>
{data.map((place: any) => (
<Card props={place} 
/>))}
//</div>
Enter fullscreen mode Exit fullscreen mode

Output

Image description

Step 5 Important Now coming to the serverAction part, where our revalidation will take place after adding a new data.

Create an async function before return in page.tsx

async function serverAction(e: FormData) {
    "use server";
    let name = e.get("name")?.toString();
    let image = e.get("image")?.toString();

    if (!name && !image) return

    const newPlace = {
      name,
      image
    }

    await fetch(process.env.MOCKAPI!, {
      method: "post",
      body: JSON.stringify(newPlace),
      headers: { "Content-Type": "application/json" },
    });

    revalidateTag('place')
  }
Enter fullscreen mode Exit fullscreen mode

Add the function's name to form action

<form action={serverAction} 
className="flex flex-col gap-2 p-1 rounded-lg bg-slate-400/10" >
Enter fullscreen mode Exit fullscreen mode

Thats all, you will see such output

Image description

Client Side (TanStack React Query)

Using Tanstack React Query provides you awesome control over data fetching and manipulation on the Client Side UI.

Step 1: Initialize a Nextjs App using terminal

npx create-next-app client-demo
 Would you like to use TypeScript? ... Yes
 Would you like to use ESLint? ... Yes
 Would you like to use Tailwind CSS? ... Yes
 Would you like to use `src/` directory? ... Yes
 Would you like to use App Router? (recommended) ... Yes
? Would you like to customize the default import alias (@/*)? » No

cd client-demo
Enter fullscreen mode Exit fullscreen mode

Step 2: Replace this code from page.tsx

"use client";

const ClientPage = () => {
  return (
    <main className="grid max-w-3xl mx-auto p-4 gap-4">
      <h1 className="text-4xl font-extrabold max-lg:text-3xl">Client Page</h1>
      <div className="grid grid-cols-3 gap-3">
        <form className="flex flex-col gap-2 p-1 rounded-lg bg-slate-400/10">
          <input
            name="name"
            placeholder="Name"
            className="p-2 rounded-md outline-none"
            type="text"
          />
          <input
            name="image"
            placeholder="Image"
            className="p-2 rounded-md outline-none"
            type="text"
          />
          <button
            type="submit"
            className="bg-purple-600 hover:bg-purple-600/90 font-extrabold uppercase text-white/70 p-2 rounded-md"
          >
            Submit
          </button>
        </form>
      </div>
    </main>
  );
};

export default ClientPage;
Enter fullscreen mode Exit fullscreen mode

Step 3: Steup Tanstack React Query

npm i @tanstack/react-query
Enter fullscreen mode Exit fullscreen mode

Create a file outside app folder provider.tsx containing following:

"use client";

import React, { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const ReactQueryProvider = ({ children }: { children: React.ReactNode }) => {
  const [queryClient] = useState(() => new QueryClient());
  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
};

export default ReactQueryProvider;
Enter fullscreen mode Exit fullscreen mode

Inside /src/app edit layout.tsx wrap children with exported ReactQureyProvider:

<body className={inter.className}>
        <ReactQueryProvider>{children}</ReactQueryProvider>
</body>
Enter fullscreen mode Exit fullscreen mode

Step 4: Using QueryClient fetching data on client side

//const ClientPage = () => {
const queryClient = useQueryClient();

  // Queries
  const { data, isLoading } = useQuery({
    queryKey: ["place"],
    queryFn: async () => {
      const res = await fetch(process.env.MOCKAPI!);
      const fetchedData = await res.json();
      return fetchedData;
    },
  });

//return (
Enter fullscreen mode Exit fullscreen mode

Adding loading state to the page after form field

//</form>
        {isLoading ? (
          <div className="flex flex-col gap-1 p-1 rounded-lg bg-slate-400/10">
            <div className="relative overflow-hidden min-h-[90px] flex-1 rounded-md bg-slate-400/30"></div>
            <span className="bg-zinc-800 font-extrabold uppercase text-white/70 p-2 rounded-md text-center animate-pulse">
              Loading
            </span>
          </div>
        ) : (
          data.map((e: any) => <Card key={e.id} props={e} />)
        )}
//</div>
Enter fullscreen mode Exit fullscreen mode

Step 5: Revalidating client side data after successfully fetching data

// Mutations
  const { mutate, isPending } = useMutation({
    mutationFn: async (newData) => {
      const res = await fetch(process.env.MOCKAPI!,
        {
          method: "post",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(newData),
        }
      );
      return res.json();
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["place"] });
    },
  });

//return (
Enter fullscreen mode Exit fullscreen mode

Adding form actions function for getting data and post it.

<form action={(e) => {
            const name = e.get("name")?.toString();
            const image = e.get("image")?.toString();

            if (!name && !image) return;

            mutate({ name, image });
          }}
          className="flex flex-col gap-2 p-1 rounded-lg bg-slate-400/10">
Enter fullscreen mode Exit fullscreen mode

Output

Image description

No Reloads!

Top comments (2)

Collapse
 
danko56666 profile image
Daniel Ko

This post is about caching but you are opting out of caching in your server fetch request and then you call a revalidate too? Why?

Collapse
 
rakshit47 profile image
Rakshit Raj • Edited

Thanks for noticing @danko56666, actually during the development period it took time to revalidate, and for getting the changes I did so, I made a mistake

instead of cache: "no-cache" it can be

 const res = await fetch(process.env.MOCKAPI!, {
    next: {
      tags: ["place"],
      revalidate: 60 * 60,
    },
  });
Enter fullscreen mode Exit fullscreen mode