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 } }
}
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.
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:
Fun With Next.js 13 Server Components
ymc9 for ZenStack ・ Nov 25 '22
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>
);
}
// 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();
}
// 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>
);
}
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>
</>
);
}
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:
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.
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>
</>
)}
</>
);
}
// 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>
);
}
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.
Top comments (4)
Can one use these features in a production app
It depends on how critical your app is. I've been playing with these features and they generally work fine, but I haven't used in production yet. I suggest wait for Next.js to move them out of Beta before using in anything important.
New features are in Beta, so I don't think you should upgrade on production for the moment.
Hey! Awesome tuto!
I need to know, what is better? using server-side data fetching or clientside for better app performance?