Learn how to fetch data with Jamstack (#20)

LR

Lee Robinson / July 01, 2020

11 min read

Hey! You're getting this email because you signed up for updates from my site. If you don't want to get these emails, definitely unsubscribe — I only want to send these to you if you look forward to getting them ✌️


A few miscellaneous updates, plus a new article!

  • React 2025 has now surpassed more than 100 pre-orders and $10,000 in revenue! Most of my time this past month has been spent recording live streams.
  • I announced the product I'll be building, Fast Feedback, in a React 2025 update.
  • Last week, I finished a week long roadtrip from Portland, OR to Yosemite National Park with my best friends. You can see some pictures here.

The rest of this email is my latest blog post about fetching data with Next.js. Should I use client-side rendering? Server-side rendering? Find out below.

Data Fetching with Jamstack#

When you're building a Jamstack (JavaScript, APIs, and Markup) app, you need to decide on your data fetching strategy. The simplest approach is to fetch data for all pages at build time. Many static site generators support this approach.

However, not all pages are created equal. As your application scales, you may need to change your data fetching strategy on a per-page basis. Maybe your landing page can be generated at build time, but your dashboard must be generated at request time. Some pages might also need to be generated after build time, but before request time.

For example, the Vercel website uses three different data fetching strategies. I'll explain what each strategy means shortly.

  • Static Generation: Generate static marketing pages, blog posts, and documentation at build time.
  • Incremental Static Generation: The blog posts are regenerated incrementally at a certain interval.
  • Client-side Fetching: On a user dashboard, it first serves a static shell, then fetches user information at request time.

To do this, you need a frontend framework that's more flexible than traditional static site generators. With Next.js, you can change your data fetching strategy on a per-page basis.

In this email, I'll explain when to use each strategy, and how you can use Next.js to implement them.

Static Generation#

Static Generation is about pre-rendering your page into a static HTML at build time. Static Generation is one of the core ideas of Jamstack and has several benefits:

  • Faster: The pre-rendered HTML files can be cached on a global CDN for performance.
  • Cheaper: On each request, the server does not need to render the page, which reduces the server load.
  • Easier: No complicated deployments. Better developer experience.

Static Generation can be done with or without external data.

Without data: In Next.js, if a page can be pre-rendered without external data, it will be pre-rendered into a HTML file at build time automatically.

// pages/products/index.js

// This page can can be pre-rendered without
// external data: It will be pre-rendered
// into a HTML file at build time.
export default function Products() {
  return <h1>Products</h1>;
}

With data: Some pages might need to fetch external data before they can be pre-rendered into a HTML file. In Next.js, you can use the following functions to specify the data dependency for each page. These functions will be called at build time.

  • getStaticProps – Fetch external data required to pre-render your page.
  • getStaticPaths (optional) – Specify which routes to generate, if you're using dynamic routes.

getStaticProps#

Let's consider an e-commerce site showing a list of all products. At build time, we want to fetch all products from a content management system (CMS) and use this data to pre-render the Products component. In Next.js, you can do so by exporting a getStaticProps function in a page component file. You can fetch external data in this function, and it will be used to pre-render the page component at build time.

export async function getStaticProps() {
  return {
    props: {
      // getProductsFromCMS() will fetch() the CMS API
      products: await getProductsFromCMS()
    }
  };
}

// Products receives products prop from getStaticProps
// at build time
export default function Products({ products }) {
  return (
    <>
      <h1>Products</h1> 
      <ul>
        {products.map((product) => (
          <li key={product.id}>{product.name}</li>
        ))}
      </ul>
    </>
  );
}

getStaticProps runs only in a build server (Node.js environment) and is not included in the client-side JavaScript bundle, so you can directly query your database if necessary.

getStaticPaths#

Your e-commerce site also needs to pre-render each product page at build time. Each product needs to have its own route based on its id, i.e. /products/[id]. In Next.js, this can be done using dynamic routes and getStaticPaths.

We also need the product data for each page. In combination with getStaticProps, we can fetch the specific product data for that id. If a product is not found given the id, it will show a 404 page (this is because of fallback: false ).

// pages/products/[id].js

export default function Product({ product }) {
  // Render product
}

// In getStaticPaths(), you need to return the list of
// ids of product pages (/products/[id]) that you'd
// like to pre-render at build time. To do so,
// you can fetch all products from your CMS.
export async function getStaticPaths() {
  const products = await getProductsFromCMS();

  const paths = products.map((product) => ({
    params: { id: product.id }
  }));

  // fallback: false means pages that don't have the
  // correct id will 404.
  return { paths, fallback: false };
}

// params will contain the id for each generated page.
export async function getStaticProps({ params }) {
  return {
    props: {
      product: await getProductFromCMS(params.id)
    }
  };
}

Incremental Static Generation#

Now, suppose that your e-commerce platform has grown significantly. Instead of 100 products, you now have 100,000. Products get updated frequently. This poses two problems:

  • Pre-rendering 100,000 pages at build time can be very slow.
  • When product data is updated, you only want to modify the affected pages. We can't have a full site rebuild every time a product is modified.

Both of these problems can be solved by Incremental Static Generation. Incremental Static Generation is about pre-rendering a subset of pages incrementally after build time. It can be used to add pages or update existing pre-rendered pages. This allows you to keep the benefits of static (always fast, always online, globally distributed) with dynamic data.

Adding Pages#

If you have 100,000 products, and pre-rendering all pages at build time is too slow, you can pre-render pages lazily.

For example, suppose that one of those 100,000 products is called product X. Using Next.js, we can pre-render this page when a user requests the page for product X. Here's how it works:

  1. A user requests the page for product X.
  2. We haven't pre-rendered this page yet. Instead of rendering 404, Next.js can serve a "fallback" version of this page (for example, show a loading indicator).
  3. In the background, Next.js will render the product X page. When that's done, the loading page will be swapped to the product X page.
  4. The next time someone else requests the page for product X, the pre-rendered product X page will be served, just like regular static generation.

To enable this behavior, you can specify fallback: true in getStaticPaths. Then, in the page itself, you can use router.isFallback to see if the loading indicator should be displayed.

export async function getStaticProps({ params }) {
  // ...
}

export async function getStaticPaths() {
  // ...

  // fallback: true means that the missing pages
  // will not 404, and instead can render a fallback.
  return { paths, fallback: true };
}

export default function Product({ product }) {
  const router = useRouter();

  if (router.isFallback) {
    return <div>Loading...</div>;
  }

  // Render product...
}

Here's an example of this behavior using Twitter as the data source.

Updating Existing Pages: Incremental Static "Re"Generation#

When a product data is updated, you wouldn't want to rebuild the entire app; only the affected pages should be updated.

For example, suppose that at build time, we've pre-rendered the page for product Y. At some point, the data for product Y gets updated. Using Next.js, we can pre-render this page again after some interval. Here's how it works:

  1. Next.js can define a "timeout" for this page – let's set it at 60 seconds.
  2. The data for product Y is updated.
  3. When the page for product Y is requested, the user will see the existing (out of date) page.
  4. When another request comes in 60 seconds after the previous request, this user will also see the existing (out of date) page, but in the background, Next.js pre-renders this page again, updating the existing HTML file.
  5. Once the pre-rendering is done, from then on Next.js serves the updated page for product Y.

To enable this behavior, you can specify unstable_revalidate: 60 in getStaticProps. It has the unstable_ prefix because as of writing, this feature is currently in beta (but ready for production).

export async function getStaticProps() {
  return {
    products: await getProductsFromCMS(),
    unstable_revalidate: 60
  };
}

Inspired by stale-while-revalidate, this ensures traffic is served statically, and new pages are pushed only after generating successfully.

Incremental Static Generation is fully supported by both next start and the Vercel edge platform out of the box.

Client-Side Fetching#

Some pages, like a user dashboard, can only be partially pre-rendered ahead of a request at build time. Most items on such pages must be rendered at request time. In such cases, you can do Client-side Fetching:

  1. Pre-render the page without data and show a loading state.
  2. Then, fetch and display the data client-side.

For fetching data, I recommend using the data fetching library SWR. It handles caching, revalidation, focus tracking, and more. For our e-commerce application, an example would be the shopping cart.

import useSWR from 'swr';

function ShoppingCart() {
  // fetchAPI is the function to do data fetching
  const { data, error } = useSWR('/api/cart', fetchAPI);

  if (error) return <div>failed to load</div>;
  if (!data) return <div>loading...</div>;
  return <div>Items in Cart: {data.products.length}</div>;
}

Server-Side Rendering#

Next.js can also pre-render a page on the server on every request – this is called Server-side Rendering. To use this feature, you can export a function called getServerSideProps from a page, just like getStaticProps.

However, I do not recommend using this strategy if you can use Incremental Static Generation or Client-side Fetching. With server rendering, the Time to First Byte (TTFB) is slower than Static Generation because the server must pre-render the page on every request. Without extra configuration, the pre-rendered pages cannot be cached by a CDN. It's also slower than Client-side Fetching because with Client-side Fetching, you'll see the loading state immediately.

Server rendering is not disappearing but evolving. Try Incremental Static Generation or Client-side Fetching and see if they fit your needs.

Also: Writing Data#

Fetching data is only half the equation. Your app might need to write data back to your data source. Adding an item to the shopping cart is a good example of this.

Next.js has a feature called API Routes for this purpose. To use this feature, you can create a file inside the pages/api directory, which creates an API endpoint we can use to mutate our data source. For example, we can create pages/api/cart.js, which accepts a productId query parameter and adds that item to our cart.

Inside our API route, we export a request handler, which receives a request and returns a json response.

export default async (req, res) => {
  const response = await fetch(`https://.../cart`, {
    body: JSON.stringify({
      productId: req.query.productId
    }),
    headers: {
      Authorization: `Token ${process.env.YOUR_API_KEY}`,
      'Content-Type': 'application/json'
    },
    method: 'POST'
  });

  const { products } = await response.json();
  return res.status(200).json({ products });
};

API routes allow us to write to an external data source securely. Using environment variables, we can include secrets for authentication without exposing the values client-side.

API routes can be deployed as Serverless Functions (which is the default when you deploy to Vercel).

Conclusion#

The hybrid approach of Next.js allows changing your data fetching strategy on a per-page basis.

  • Use Static Generation and Incremental Static Generation to pre-render pages ahead of a request.
  • Use Client-side Fetching if you need to render a page at request time.
  • Mutate data using API routes.

For more information, check out the Next.js documentation.

If you'd like to follow along with React 2025, check out Fast Feedback as well as the GitHub repo. It's not much yet, but it's growing quickly! Also, feel free to reach out if you have any questions. I look forward to hearing from you!

Subscribe to the newsletter

Get emails from me about web development, tech, and early access to new articles.

Spotify album cover

/uses/photos/newsletter