Fetching Google Sheets Data in Next.js Server Components
How to use React Server Components and Next.js fetch caching to serve Google Sheets data with zero client JavaScript.
Why Server Components are the right model for Sheets data
The old pattern for fetching external data in React: useEffect, a loading state, a fetch call from the browser, a spinner while you wait. The problem with that pattern for Google Sheets data is that it moves the work to the wrong place — it runs in the browser, appears in the client bundle, and adds a loading state to every page visit.
Server Components run on the server at request time (or build time, depending on your caching strategy). They fetch the data, render the HTML, and send it to the browser. The browser receives a complete page — no JavaScript for the data fetch, no loading spinner, no empty shell waiting for content to arrive.
For a spreadsheet-backed page — a team directory, a pricing table, a content listing — this is the right model. The data is not user-specific and it changes slowly. Fetching it on the server once and caching the result is exactly what the page needs.
Basic fetch with ISR revalidation
Next.js extends the native fetch API with a next option for caching behavior. Passing revalidate enables Incremental Static Regeneration: the page is cached and served statically, and regenerated in the background after the specified number of seconds.
// app/team/page.tsx
export default async function TeamPage() {
const res = await fetch(
"https://sheetsapi.gkit.mreshank.com/api/spreadsheets/USER_KEY/Team",
{ next: { revalidate: 60 } }
);
if (!res.ok) {
throw new Error("Failed to fetch team data");
}
const { data } = await res.json();
return (
<main>
<h1>The team</h1>
<ul>
{data.map((member: { name: string; role: string }) => (
<li key={member.name}>
<strong>{member.name}</strong> — {member.role}
</li>
))}
</ul>
</main>
);
}With revalidate: 60, content editors update the Sheet and the page reflects the change within a minute, with no deployment. The server handles all the fetching — zero client bundle impact.
Streaming with Suspense for slow sheets
Large sheets or sheets with many formulas can take a moment to respond. Rather than blocking the whole page render, wrap the slow component in Suspense and stream it in when ready:
// app/pricing/page.tsx
import { Suspense } from "react";
import { PricingTable } from "./pricing-table";
export default function PricingPage() {
return (
<main>
<h1>Pricing</h1>
<Suspense fallback={<p>Loading plans…</p>}>
<PricingTable />
</Suspense>
</main>
);
}// app/pricing/pricing-table.tsx
export async function PricingTable() {
const res = await fetch(
"https://sheetsapi.gkit.mreshank.com/api/spreadsheets/USER_KEY/Pricing",
{ next: { revalidate: 300 } }
);
const { data } = await res.json();
// render table with data
}The page shell renders immediately. The pricing table streams in when the fetch resolves. The fallback is visible only if the fetch takes long enough to matter.
Error boundaries for production resilience
A Server Component that throws an error will crash the whole page unless you add an error boundary. Create error.tsx alongside the page:
// app/team/error.tsx
"use client";
export default function TeamError() {
return <p>Could not load team data. Try refreshing the page.</p>;
}The "use client" directive is required on error boundaries — they need to catch errors at the client boundary. The rest of the page renders normally; only the failing component is replaced by the fallback.
The net result
A team page backed by a Google Sheet, built with Server Components: the browser receives complete HTML, the Google API call never appears in the network tab, the loading state is a Suspense fallback only when the data is genuinely slow, and content editors update the page without touching the codebase. That is the whole workflow — and none of it required a database.
Connect a Sheet to SheetsAPI, copy the endpoint URL, and drop it into your Server Component's fetch call. The caching layer handles the rest.