← Back to list

How to Use Google Sheets with React Using SheetDB

Fetch, display, and submit data from Google Sheets in your React app — with smart caching that saves you money.

Chris Switalski in SheetDB · 2026-04-13 09:55 · 4 claps · 9.0 min read
#sheetdb #react #javascript #jsx #cache
Open on Medium ↗
Wiki topics: ECO · Economy · General 🌐 · Web Development

How to Use Google Sheets with React Using SheetDB

Fetch, display, and submit data from Google Sheets in your React app — with smart caching that saves you money.

Your React app needs dynamic content — a pricing table, a list of team members, a product catalog. The usual path is: set up a database, build an API, deploy a backend. But what if the people updating that data aren’t developers? What if they just want to open a spreadsheet and type?

Google Sheets + SheetDB gives your React app a full REST API backed by a spreadsheet. No backend code, no database, no deployment pipeline for content changes. Your marketing team updates a Sheet, your React app shows the new data.

In this guide, we’ll build two real-world React examples, create a reusable hook with browser-side caching, and explain exactly when and how to cache — and why it matters for your bill.

Why React + SheetDB?

SheetDB turns your Google Sheet into a REST API with full CORS support -meaning your React app running in the browser can call it directly. No proxy server, no serverless functions, just fetch().

Getting Started: Your First API Call in React

Step 1: Prepare Your Sheet

Create a Google Sheet with headers in the first row. These headers become your JSON field names.

Tip: Use snake_case headers — they’ll be cleaner in your JavaScript code.

Step 2: Create Your SheetDB API

  1. Go to https://sheetdb.io and sign in.
  2. Click Create New API and paste your Google Sheet URL.
  3. Copy your API ID. Your endpoint is:
https://sheetdb.io/api/v1/YOUR_API_ID

Step 3: Fetch Data in React

import { useState, useEffect } from 'react';

const SHEETDB_URL = 'https://sheetdb.io/api/v1/YOUR_API_ID';

function ProductList() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(SHEETDB_URL)
      .then(res => res.json())
      .then(data => {
        setProducts(data);
        setLoading(false);
      });
  }, []);

  if (loading) return <p>Loading products...</p>;

  return (
    <ul>
      {products.map(product => (
        <li key={product.name}>
          <strong>{product.name}</strong> — ${product.price}
          <br />
          <small>{product.description}</small>
        </li>
      ))}
    </ul>
  );
}

That’s it. No Axios, no library - just native fetch. SheetDB returns a JSON array, and React renders it.

Caching: The Difference That Saves You Money

SheetDB’s Built-In Cache

SheetDB caches responses on its servers (default: 15 seconds, configurable in your dashboard). This protects the Google Sheets API from rate limits and speeds up responses.

But here’s the thing: SheetDB cache does not reduce your request count. Every time your React app calls the API — even if the response is served from SheetDB’s cache — it counts as a request against your plan. The response might come back faster, but you still pay for it.

Browser-Side Cache (What You Should Also Do)

Browser-side caching stores the API response in your user’s browser — in memory, localStorage, or sessionStorage. When the user navigates between pages or refreshes, the app loads data instantly from the local cache instead of making another API call.

This is the cache that saves you money:

  • A user refreshing your page 5 times in 10 minutes? 1 request instead of 5.
  • A user navigating between your product page and homepage? 1 request total.
  • 1,000 users visiting your site, each browsing for 15 minutes? With a 1-hour TTL, you pay for 1,000 requests instead of potentially 5,000+.

The Trade-Off: Stale Data

Browser caching introduces one risk: your users might see outdated content. If someone updates the Google Sheet, users with cached data won’t see the change until their cache expires.

Comparison at a Glance

The best approach: use both. SheetDB cache handles infrastructure-level performance. Your browser cache handles cost optimization and user experience.

A Reusable Hook: useSheetDB

Let’s build a custom React hook that wraps fetch with smart in-memory + localStorage caching. You can reuse this across your entire app.

import { useState, useEffect } from 'react';

// In-memory cache (survives re-renders, cleared on page reload)
const memoryCache = {};

function useSheetDB(apiId, options = {}) {
  const {
    sheet = null,           // specific sheet/tab name
    search = null,          // search params, e.g. { category: 'software' }
    ttl = 60 * 60 * 1000,   // cache TTL in ms (default: 1 hour)
    persist = true,          // also cache in localStorage
  } = options;

  // Build the URL
  const url = buildUrl(apiId, { sheet, search });
  const cacheKey = `sheetdb_${url}`;

  // Initialize from memory cache synchronously (no loading flash on cache hit)
  const cached = memoryCache[cacheKey] && Date.now() - memoryCache[cacheKey].ts < ttl
    ? memoryCache[cacheKey].data
    : null;

  const [data, setData] = useState(cached);
  const [loading, setLoading] = useState(!cached);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 1. Check in-memory cache first (fastest)
    if (memoryCache[cacheKey] && Date.now() - memoryCache[cacheKey].ts < ttl) {
      setData(memoryCache[cacheKey].data);
      setLoading(false);
      return;
    }

    // 2. Check localStorage (survives page reloads)
    if (persist) {
      try {
        const stored = localStorage.getItem(cacheKey);
        if (stored) {
          const parsed = JSON.parse(stored);
          if (Date.now() - parsed.ts < ttl) {
            memoryCache[cacheKey] = parsed; // promote to memory
            setData(parsed.data);
            setLoading(false);
            return;
          }
        }
      } catch {
        // localStorage unavailable or corrupted — continue to fetch
      }
    }

    // 3. Fetch from SheetDB
    const controller = new AbortController();
    setLoading(true);
    setError(null);

    fetch(url, { signal: controller.signal })
      .then(res => {
        if (!res.ok) throw new Error(`SheetDB error: ${res.status}`);
        return res.json();
      })
      .then(json => {
        const entry = { data: json, ts: Date.now() };
        memoryCache[cacheKey] = entry;
        if (persist) {
          try { localStorage.setItem(cacheKey, JSON.stringify(entry)); } catch {}
        }
        setData(json);
        setLoading(false);
      })
      .catch(err => {
        if (err.name === 'AbortError') return;
        setError(err.message);
        setLoading(false);
      });

    return () => controller.abort();
  }, [url, ttl, persist]);

  return { data, loading, error };
}

function buildUrl(apiId, { sheet, search }) {
  const base = `https://sheetdb.io/api/v1/${apiId}`;

  if (search && Object.keys(search).length > 0) {
    const params = new URLSearchParams(
      Object.entries(search).sort(([a], [b]) => a.localeCompare(b))
    );
    if (sheet) params.set('sheet', sheet);
    return `${base}/search?${params}`;
  }

  if (sheet) return `${base}?sheet=${encodeURIComponent(sheet)}`;
  return base;
}

export default useSheetDB;

Usage:

// Fetch all rows (cached for 1 hour)
const { data: products, loading } = useSheetDB('YOUR_API_ID');

// Fetch from a specific sheet tab
const { data: team } = useSheetDB('YOUR_API_ID', { sheet: 'team' });

// Search for specific rows
const { data: software } = useSheetDB('YOUR_API_ID', {
  search: { category: 'software' },
});

// Short TTL for frequently updated data
const { data: stock } = useSheetDB('YOUR_API_ID', {
  search: { product: 'Widget Pro' },
  ttl: 30 * 1000, // 30 seconds
});

// Disable localStorage persistence (memory-only cache)
const { data: notices } = useSheetDB('YOUR_API_ID', {
  persist: false,
  ttl: 5 * 60 * 1000, // 5 minutes
});

The hook checks memory first (instant, no parsing), then localStorage (survives reloads), and only calls the API if both caches miss or are expired. You control the TTL per use case.

Use Case 1: Product Catalog with Live Search

Let’s build a real product catalog that reads from a Google Sheet, supports filtering, and caches smartly.

Your Google Sheet:

The React component:

import { useState } from 'react';
import useSheetDB from './useSheetDB';

function ProductCatalog() {
  const [category, setCategory] = useState(null);

  // When category changes, a new cache key is created automatically
  const { data: products, loading, error } = useSheetDB('YOUR_API_ID', {
    search: category ? { category } : null,
    ttl: 30 * 60 * 1000, // 30 minutes — products don't change often
  });

  if (error) return <p>Failed to load products: {error}</p>;

  return (
    <div>
      <h1>Our Products</h1>

      <div style={{ marginBottom: 16 }}>
        <button onClick={() => setCategory(null)}>All</button>
        <button onClick={() => setCategory('software')}>Software</button>
        <button onClick={() => setCategory('hardware')}>Hardware</button>
      </div>

      {loading ? (
        <p>Loading...</p>
      ) : (
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 16 }}>
          {products.map(product => (
            <div
              key={product.name}
              style={{
                border: '1px solid #e0e0e0',
                borderRadius: 8,
                padding: 16,
                width: 280,
              }}
            >
              <h3>{product.name}</h3>
              <span style={{
                background: '#f0f0f0',
                borderRadius: 4,
                padding: '2px 8px',
                fontSize: 12,
              }}>
                {product.category}
              </span>
              <p>{product.description}</p>
              <strong>${product.price}</strong>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

When a user clicks “Software”, the hook calls SheetDB’s /search?category=software endpoint — but only the first time. Switch back and forth between filters, and the cached results load instantly. No extra API calls, no loading spinners.

Use Case 2: Contact Form That Writes to Google Sheets

Reading data is only half the story. SheetDB also supports POST — so your React app can write data directly to a Google Sheet. This is perfect for contact forms, surveys, feedback, or order submissions.

Your Google Sheet (initially empty, just headers):

The React form:

import { useState } from 'react';

const SHEETDB_URL = 'https://sheetdb.io/api/v1/YOUR_API_ID';

function ContactForm() {
  const [status, setStatus] = useState('idle'); // idle | sending | sent | error
  const [form, setForm] = useState({ name: '', email: '', message: '' });

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

  const handleSubmit = async (e) => {
    e.preventDefault();
    setStatus('sending');

    try {
      const res = await fetch(SHEETDB_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          data: {
            timestamp: new Date().toISOString(),
            name: form.name,
            email: form.email,
            message: form.message,
            source: window.location.href,
          },
        }),
      });

      if (!res.ok) throw new Error('Failed to submit');

      setStatus('sent');
      setForm({ name: '', email: '', message: '' });
    } catch {
      setStatus('error');
    }
  };

  if (status === 'sent') {
    return <p>Thanks! Your message has been received.</p>;
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Name</label>
        <input
          id="name"
          name="name"
          value={form.name}
          onChange={handleChange}
          required
        />
      </div>
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          name="email"
          type="email"
          value={form.email}
          onChange={handleChange}
          required
        />
      </div>
      <div>
        <label htmlFor="message">Message</label>
        <textarea
          id="message"
          name="message"
          value={form.message}
          onChange={handleChange}
          required
        />
      </div>
      <button type="submit" disabled={status === 'sending'}>
        {status === 'sending' ? 'Sending...' : 'Send Message'}
      </button>
      {status === 'error' && <p style={{ color: 'red' }}>Something went wrong. Please try again.</p>}
    </form>
  );
}

Every form submission creates a new row in your Google Sheet. Your team sees responses immediately — no admin panel needed. Sort, filter, and share the data using all the tools Google Sheets already provides.

Security tip: For a public-facing form, enable only Create permission in your SheetDB dashboard and disable Read, Update, and Delete. This way the form can write data but nobody can read or modify existing entries through the API.

Bonus: Use TIMESTAMP for Automatic Timestamps

SheetDB supports special values. Instead of generating a timestamp in JavaScript, you can let SheetDB handle it:

body: JSON.stringify({
  data: {
    timestamp: 'DATETIME', // SheetDB fills in "2026-04-13 14:30:15"
    name: form.name,
    email: form.email,
    message: form.message,
  },
}),

This ensures consistent server-side timestamps regardless of the user’s local clock.

Production Tips

In your SheetDB dashboard, configure permissions per API:

  • Product catalog (read-only) → Enable only Read and Search
  • Contact form (write-only) → Enable only Create

Never give more permissions than necessary.

Use Bearer Token for Sensitive APIs

If your API contains anything private, add authentication:

fetch(url, {
  headers: {
    'Authorization': 'Bearer YOUR_SHEETDB_TOKEN',
  },
})

Note: Bearer tokens in client-side React code are visible in the browser’s DevTools. For truly sensitive data, proxy requests through your backend. For most use cases (product catalogs, public content), direct browser calls are fine.

Handle Rate Limits

SheetDB allows 15 requests per 10 seconds per IP. With browser caching in place, you’re unlikely to hit this — but handle it gracefully:

// In your useSheetDB hook or fetch wrapper
if (res.status === 429) {
  // Retry after a delay
  await new Promise(r => setTimeout(r, 2000));
  return fetch(url); // retry once
}

Clear Cache When Needed

Sometimes you want to force a refresh — after a form submission, for example:

function clearSheetDBCache(apiId) {
  const prefix = `sheetdb_https://sheetdb.io/api/v1/${apiId}`;
  for (const key of Object.keys(localStorage)) {
    if (key.startsWith(prefix)) {
      localStorage.removeItem(key);
    }
  }
  // Also clear memory cache
  for (const key of Object.keys(memoryCache)) {
    if (key.startsWith(prefix)) {
      delete memoryCache[key];
    }
  }
}

Wrapping Up

React + SheetDB is a surprisingly powerful combination. You get a full-featured API backed by a spreadsheet that anyone can edit — and with smart browser caching, you keep costs low and user experience fast.

What we covered:

  • Fetching Google Sheet data in React with plain fetch()
  • The critical difference between SheetDB cache (server-side, doesn’t reduce costs) and browser cache (client-side, saves requests and money)
  • A reusable useSheetDB hook with layered caching (memory → localStorage → API)
  • A product catalog with search and filtering
  • A contact form that writes to Google Sheets

The pattern works for landing pages, dashboards, internal tools, MVPs, and anything where your data lives in a spreadsheet. Start simple, cache smart, and let your non-technical team own the data.

Happy Coding!


메타데이터
post_id
abfea8bd0e97
slug
how-to-use-google-sheets-with-react-using-sheetdb-abfea8bd0e97
url
https://blog.sheetdb.io/how-to-use-google-sheets-with-react-using-sheetdb-abfea8bd0e97
canonical_url
https://blog.sheetdb.io/how-to-use-google-sheets-with-react-using-sheetdb-abfea8bd0e97
author_url
https://medium.com/@chris-switalski
status
ok
fetched_at
2026-06-11 17:15:47