Next.js 13 - Data fetching

New Linkcart Checkout

Intro

Now before we get into this, it should be noted that quite literally everything I am writing about in this blog post is covered in the Next.js docs and I strongly recommend you go spend and afternoon with a cup of tea and just bathe in the awesomeness that is Next.js 13 and absorb as much of it as you can.

This article expects that you have a solid understanding of Next.js as a web development framework and it will focus on one of the most notable changes announced with the major version upgrade, and that is Data Fetching.

The current state of play

Before we get started with the new, it's important to recognise where we are. Next.js 13 took over gatsby for me as my favorite tool to build websites with when the development team announced the ability to have both Server Side Rendering (SSR) and Static Site Generation (SSG) running in the same application. It was completely mind boggling, and at the time distinctly I recall thinking:

“Why would you use anything else?” - a young Blake

To this day, getServerSideProps, getStaticProps and getStaticPaths are widely used for data fetching on Next.js applications and are basically the backbone (beyond client side data fetching) of the framework. The functions are appropriately named, easy to remember and have had their functions extended to include the ability to control fallback and timeouts for cache invalidation.

It is at this point in time i'm going to take something quite literally out of the Next.js docs on Data fetching:

Previous Next.js data fetching methods such as getServerSideProps, getStaticProps, and getInitialProps are not supported in the new app directory.

So where does that leave developers? How the f*** do we get data from an API?

Introducing the fetch() API (also appropriately named)

In my opinion, one the most groundbreaking features of Next.js 13 is moving to using fetch() over the aforementioned methods, is the ability that fetch() has to promote Request Deduping and the way it handles static and dynamic data fetches.

Request Deduping

  • On the server, the cache lasts the lifetime of a server request until the rendering process completes.
  • On the client, the cache lasts the duration of a session (which could include multiple client-side re-renders) before a full page reload.
  • fetch() has already been patched to include support for cache() automatically, so you don't need to wrap functions that use fetch() with cache().

cache() --> more information can be found here.

Static and Dynamic Data Fetching

  • Static Data is data that doesn't change often. For example, a blog post.
  • Dynamic Data is data that changes often or can be specific to users. For example, a shopping cart list.

You can find more information about Request Deduping and Static and dynamic data fetching in the official Next.js 13 docs. There's a great write up on how developers are in control of cache, more now than ever before.

Data fetching - Static and on the server

One of the biggest utilities of simplifying data fetching in Next.js is the standardisation of request implementation and structure when interacting with third party APIs - regardless of component / page. It's not uncommon for developers to side with apollo, axios or node fetch in different projects, but with fetch() replacing getServerSideProps and getStaticProps methods entirely, we will see a common standard put in place for the framework which is pre optimised.

It also suggests when coupled with Streaming and Suspense patterns, that Next.js is pushing developers to make requests in the components that actually need them - reducing the amount of props passing down the component chain, and increasing the number of parallel requests. Below you'll find a quick list of examples on how you can use the new fetch() function to make requests in a variety of different ways.

Make a request, with no cache expiry:

// This request should be cached until manually invalidated.
// Similar to `getStaticProps`.
fetch('https://...') // cache: 'force-cache' is the default

Make a request, and revalidate on a 10 second interval:

// This request should be cached with a lifetime of 10 seconds.
// Similar to `getStaticProps` with the `revalidate` option.
fetch('https://...', { next: { revalidate: 10 } })

Make a request on every visit (no cache):

// This request should be refetched on every request.
// Similar to `getServerSideProps`.
fetch('https://...', { cache: 'no-store' })

Good to know:

  • If an individual fetch() request sets a revalidate number lower than the default revalidate of a route, the whole route revalidation interval will be decreased.
  • If two fetch requests with the same URL in the same route have different revalidate values, the lower value will be used.
  • As a convenience, it is not necessary to set the cache option if revalidate is set to a number since 0 implies cache: 'no-store' and a positive value implies cache: 'force-cache'.
  • Conflicting options such as { revalidate: 0, cache: 'force-cache' } or { revalidate: 10, cache: 'no-store' } will cause an error.

What about getStaticPaths?

*generateStaticParams entered the chat..*

generateStaticParams is the new getStaticPaths. It behaves extrememly similarly to its predecessor, as it runs at build time before the corresponding Layouts or Pages are generated, so it will not be called again during revalidation (ISR).

The value returned by generateStaticParams is used to generate a list of static segments, which each receive the value of the returned object as their params prop, see the example below.

export async function generateStaticParams() {
  const products = await getAllProducts();

  return products.map((product) => ({
    handle: product.handle,
  }));
}

export default function Product({ params }) {
  const { handle } = params;

  return ...
}

Patterns

Now we know how to use fetch() independantly, what about a real life application?

Sequential data fetching

Consider the following example 🤔. The product page has a page (page.js) level request to Shopify via the Shopify Storefront API for a single product.

app\product\[slug]\page.js

import { ProductView } from '@/components/product'
import { getProduct } from '@platform/product'

export default async function Product({ params }) {
  const variables = { handle: params.slug }
  const productData = await getProduct(variables, { revalidate: 1000 })
  const [product] = await Promise.all([productData])

  return <ProductView product={product} />
}

export const revalidate = 14400 // revalidate this page every 4 hours

This page utilises a 4 hour cache, meaning that for most customers the load time should be basically instant, and there should be no need to query Shopify for any data at all.

It's not uncommon for product pages these days to have a degree of up-selling or cross-selling. For our example, we're going to create a cross-sell recommendation that executes on every single request, utilising no cache at all. This type of supplementary request typically would occur client-side. The first thing that should come to mind is:

"Wait, but we have this awesome static product page thats super fast and now were going to block the rendering of this page for the cross-sell products?"

Using the new features of Next.js 13, we can still deliver the original cached product page and use serverside components and Streaming to make our request to the recommendations API, then hydrate the user interface with the cross-sell products. See the following code:

components\product\ProductView.js
...
  </div>
      <Suspense fallback={<p>Loading product recommendations...</p>}>
        <ProductCarousel product={product} />
      </Suspense>
    </div>
  </div>
...

What makes this so performant is that the browser immediately receives the product page without having to load for the product recommendations. This means any impact on the Time-to-Interactive (TTI) or the Total-Blocking-Time (TBT) is mitigated and all logic required to fetch the product recommendations stays on the server, decreasing the bundle size sent to the client. You can see the sequential request that fires on the server side below.

components\product\ProductCarousel.js

import { getProductRecommendations } from "@platform/product";
import React from "react";
import Link from "next/link";

async function getRecommendations(id) {
  const variables = { productId: id };
  const productsData = await getProductRecommendations(variables, { cache: 'no-store' });
  return productsData;
}

export const ProductCarousel = async ({ product }) => {
  const { id } = product;
  const relatedProductsData = await getRecommendations(id);
  // Wait for the promises to resolve
  const [relatedProducts] = await Promise.all([relatedProductsData]);
...

Once the above code executes and the Ui is rendered on the server, the server then pushes to the client side (we call this hydration).

Parallel data fetching

Where Next.js 13 really steps into its stride is when we think more holistically about the data that frequently appears on product pages, such as reviews, UGC, recommendations, CMS content (the list goes on..) and how traditionally the approach was to statically generate pages as much as possible to reduce load times. With parallel data fetching we can decrease our dependancy on SSG and move more to a hybrid approach.

Take the following code as an example.

app\product\[slug]\page.js

import { ProductView } from '@/components/product'
import { getProduct } from '@platform/product'

export default async function Product({ params }) {
  const variables = { handle: params.slug }
  // Initiate both requests in parallel
  const productData =  getProduct(variables)
  const sesssionRecommendationData =  getSessionRecommendations(variables)
  // Wait for the promises to resolve, the user wont see the rendered result until the following promises are resolved.
  const [product, recommendations] = await Promise.all([productData,sesssionRecommendationData])

  return <ProductView product={product} recommendations={recommendations} />
}

export const revalidate = 14400 // revalidate this page every 4 hours

Summary

Next.js 13 doubles down on their commitment to performance, but also gives developers the ability to control how they're using the framework itself, rather than forcing them to follow strict conventions. Developers are spoiled for choice, and the introduction of the now extended multi-purpose fetch() Web API is a clear example of that. Much of Next.js 13 is still considered unstable, but it's a great time to play around and get used to the changes like fetch() and the new app/ directory.

Extra

As a side note, when reading through the Next.js 13 documentation I found this awesome pattern called "Preload" which utilises cache(), you can check out the documentation on that here