Deep Dive Into Next.js 13 Data Fetching

ymc9 - Dec 9 '22 - - Dev Community

Next.js 13 has landed in a somewhat confusing way. Many remarkable things have been added; however, a good part is still Beta. Nevertheless, the Beta features give us important signals on how the future of Next.js will be shaped, so there are good reasons to keep a close eye on them, even if you're going to wait to adopt them.

This article is part of a series of experiences about the Beta features. Today, let's look into the least mature part - the new data-fetching patterns.


Pre Next.js 13, page-level data-fetching patterns are pretty straightforward:

  • If your page is (mostly) static, implement a getStaticProps to fetch data so that the fetching happens at build time (and at ISR time).
  • If your page is dynamic, implement a getServerSideProps to fetch data per request on the server side.
  • For data that depends on user interaction, do fetching on the client side within the useEffect hook after the page is rendered.

This has been completely renovated in Next.js 13 (if you opt for the experimental features). But before we see the new stuff, let’s reflect on issues with the old world.

Problems with the old patterns

1. It looks unnatural

export default function Page({ data }) {
  // Render data...
}

export async function getServerSideProps() {
  const res = await fetch(`https://.../data`)
  const data = await res.json()
  return { props: { data } }
}
Enter fullscreen mode Exit fullscreen mode

The sole purpose of getServerSideProps is to compute the props for Page, yet you have to put them side-by-side as two separate functions. I’m not generally against convention-based APIs, but this pattern looks unnecessarily verbose.

2. It quite often incurs "prop drilling"

"Prop drilling" is when you have a complex state object somewhere very high in the render tree and need to pass down pieces of it (or the whole) deep down. It’s both cumbersome to write and hard to read.

https://blog.logrocket.com/solving-prop-drilling-react-apps/

 

Although prop drilling is not specific to SSR or SSG, data-fetching functions - getStaticProps and getServerSideProps - are natural sources of it. Because when adopting SSR/SSG, you want to fetch everything possible on the server side for a page, and the data fetching pattern requires it to happen in one centralized place; you’ll have to propagate a big chunk of props down the tree.

There’re remedies for prop drilling, like using Context API or component composition pattern, but that either hinders the reusability of your components or requires you to design them carefully.

To put it in another word, the root cause of the problem is that data fetching cannot collocate with the components that use the data.

3. It’s all-or-nothing

When using getServerSideProps, whether a page loading is triggered by a browser reload or a client-side routing, the new page content is not shown until the data fetching is fully completed (the async getServerSideProps function resolves). This can be problematic if your page contains both "quick data" and "slow data". For example, data dashboard is a typical scenario: some cards can load instantaneously, while others can take many seconds.

What’s revamped in Next.js 13

The new data fetching patterns introduced in Next.js 13 dropped everything you’re familiar with: getStaticProps, getServerSideProps, and even for client-side fetching, there’s a new use hook that’ll potentially replace the old way of fetching in useEffect.

The new patterns are deeply coupled with React Server Components. If you’re unfamiliar with it, it’s good to check out the previous article in the series:

Async server components

In the old world, components were synchronous, and you couldn’t await at the top level in a component. In Next.js 13, all components are by default "server" and can be asynchronous. Finally we can use our familiar async/await syntax in React components.

This makes data fetching so much easier and more flexible. The best thing is that you can distribute your server-side fetching logic into multiple places and collocate them with components that use the data.

Let’s look at an example with two components both fetching random quotes from API:

// app/server-async-fetching/page.js

// a page containing a fast-loading and a slow-loading components,
// both are server components

import Quote from '../../components/server/Quote';

export default function AsyncLoading() {
    return (
        <div>
            <Quote />
            <Quote slow={true} />
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode
// lib/quote.js

// utility for fetching random famous quotes from API, allowing simulation of a
// slow request

import sleep from 'sleep-promise';

export async function getQuote(delay = 0) {
    if (delay) {
        await sleep(delay);
    }
    return (
        await fetch('https://api.quotable.io/random?tags=technology')
    ).json();
}
Enter fullscreen mode Exit fullscreen mode
// components/server/Quote.js

import { getQuote } from '../../lib/quote';
import os from 'os';

export default async function Quote() {
    const quote = await getQuote(slow ? 2000 : 0);
    return (
        <div>
            <p>
                {slow ? 'Slow' : 'Fast'} component rendered on
                <span>${os.hostname()}</span>
            </p>
            <blockquote>
                {quote.content}
            </blockquote>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

Image description

This worked, with cleaner async/await code; however, it still suffers from the "all-or-nothing" problem. The page is only rendered when both the Fast and Slow components finish fetching. We can improve it with a small fix by adding <Suspense /> around the components. Suspense was initially added by React to support code splitting; now it can be used to provide a fallback UI for async components that aren’t resolved yet, so they can render without blocking:

export default function AsyncLoading() {
    return (
        <>
            <div>
                <Suspense fallback={<p>Fast component loading...</p>}>
                    <Quote />
                </Suspense>

                <Suspense fallback={<p>Slow component loading...</p>}>
                    <Quote slow={true} />
                </Suspense>
            </div>
        </>
    );
}
Enter fullscreen mode Exit fullscreen mode

Image description

Much better now. You can see that the rendering of the page and its two child components are entirely asynchronous. React has extended the power of Suspense to support arbitrary asynchronous operation. It now works perfectly well with async server components. What’s cool about Suspense is that "unsuspending" a component doesn’t take extra API requests or a WebSocket connection. Instead, the new page content is streamed to the browser by appending virtual DOM (wrapped in <script/>) to the HTML document. This can be confirmed by looking at the timing of the document request:

Image description

Automatically deduped fetch

Another interesting thing you might already notice is that although the Fast and Slow components made API requests (with fetch) separately, they got the same quote content. This is due to another important update from React - fetch call (on the server side) is deduped automatically:

If you need to fetch the same data in multiple components in a tree (e.g. current user), Next.js will automatically cache fetch requests that have the same input in a temporary cache.


https://beta.nextjs.org/docs/data-fetching/fundamentals#automatic-fetch-request-deduping

 

This helps us resolve the other issue with the old data fetching patterns - props drilling. With automatic fetch deduping, fetching the same resource in a render pass generates only one single HTTP request, so you’ve got the freedom to fetch data right where you need to render it without worrying about the extra cost. Pretty cool, isn't it?

Client-side data fetching

In previous versions of Next.js (and React), client-side data fetching is out of the framework's concern. You can use whatever library you want, and third-party tools like SWR and react-query did a fantastic job solving this problem.

Next.js 13 (should more fairly say the latest React) made a step forward by offering a built-in use hook as a general API for unwrapping data from promises. It’s not as ideal as directly using async/await (as explained by React), but it makes client-side fetching feels close enough to the server side.

Let’s again see how it works with an example (all components client components as they’re marked by ‘use client’):

// app/client-fetching/page.js

'use client';

import { useState, Suspense } from 'react';
import Quote from '../../components/client/Quote';

export default function ClientFetching() {
    // use a button to toggle the loading of components to make sure 
    // they're rendered on the client-side
    const [show, setShow] = useState(false);

    return (
        <>
            <h1>Client Fetching</h1>
            <button onClick={() => setShow(true)}>
                Show Components
            </button>

            {show && (
                <>
                    <div>
                        <Suspense fallback={<p>Fast component loading...</p>}>
                            <Quote />
                        </Suspense>

                        <Suspense fallback={<p>Slow component loading...</p>}>
                            <Quote slow={true} />
                        </Suspense>
                    </div>
                </>
            )}
        </>
    );
}
Enter fullscreen mode Exit fullscreen mode
// components/client/Quote.js

'use client';

import { getQuote } from '../../lib/quote';
import { use } from 'react';

const quoteFetch = getQuote();
const quoteFetchSlow = getQuote(2000);

export default function Quote({ slow }) {
    const quote = use(slow ? quoteFetchSlow : quoteFetch);
    return (
        <div>
            <p>{slow ? 'Slow' : 'Fast'} component rendered</p>
            <blockquote>{quote.content}</blockquote>
        </div>
    );
}

Enter fullscreen mode Exit fullscreen mode

Image description

It's cleaner than what we were used to: do fetching in useEffect and store the result in a state variable. It's also close enough to how it looks in server components.

A cautious reader might have noticed the result looked different from what we saw with server components. You're right; the two components rendered two different quotes. The fetch call is not deduped on the client side. I don't know if this is by design or something to be fixed (I hope so). However, since the use hook is generic and works with any Promise, I suppose you can implement a cache layer around fetch without a problem.

A long way ahead

The new data fetching pattern is a huge change and looks exciting but, at the same time, a bit confusing. The documentation contains vague descriptions about what can or cannot be done in multiple places. Typescript support is also incomplete. I think it’s mainly because its foundation - React’s async server component and use hook - are so new and unpolished. There is still a long way ahead until this part is ready for production use.

I like the idea of async server components. It's an excellent way to move more computations to the server side, reduce client bundle size, and preserve a relatively consistent programming model across the network border. However, at the same time, I also anticipate encountering lots of confusion, anti-pattern, and mysterious bugs when I start to adopt it seriously 😄.

You can find the demo project code here.

At the end

Thank you for your time reading to the end ❤️.

This is the third article of a series about the Beta features of Next.js 13. You can find the entire series here. If you like our posts, remember to follow us for updates at the first opportunity.


P.S. We're building ZenStack — a toolkit for building secure CRUD apps with Next.js + Typescript. Our goal is to let you save time writing boilerplate code and focus on building what matters — the user experience.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .