Asynchronous Data Fetching
In Nextbase starter kit, we to rely on server components as much as possible to fetch data. The advantages are
- smaller bundle sizes: because we don't have to ship the data fetching logic to the client
- faster page loads: because we rely on the cpu power of the server instead of the client to fetch data. Server CPUs are usually faster than client CPUs.
Data Fetching in Server components
Since server components can by async, we can just await on the data which we require for that component. Next.js layouts and pages are server components by default unless you explicitly specify them as client components.
For instance, take a look at our the page rendering our /blog/[slug]
route:
import {
anonGetPublishedBlogPostBySlug,
anonGetPublishedBlogPosts,
} from '@/data/anon/internalBlog';
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { z } from 'zod';
const paramsSchema = z.object({
slug: z.string(),
});
// Return a list of `params` to populate the [slug] dynamic segment
export async function generateStaticParams() {
const posts = await anonGetPublishedBlogPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
export async function generateMetadata({
params,
}: {
params: unknown;
}): Promise<Metadata> {
// read route params
const { slug } = paramsSchema.parse(params);
const post = await anonGetPublishedBlogPostBySlug(slug);
return {
title: `${post.title} | Blog | Nextbase Boilerplate`,
description: post.summary,
openGraph: {
title: `${post.title} | Blog | Nextbase Boilerplate`,
description: post.summary,
type: 'website',
images: post.cover_image ? [post.cover_image] : undefined,
},
twitter: {
images: post.cover_image ? [post.cover_image] : undefined,
title: `${post.title} | Blog | Nextbase Boilerplate`,
card: 'summary_large_image',
site: '@usenextbase',
description: post.summary,
},
};
}
export default async function BlogPostPage({ params }: { params: unknown }) {
try {
const { slug } = paramsSchema.parse(params);
const post = await anonGetPublishedBlogPostBySlug(slug);
return (
<div className="relative w-full space-y-8 px-4 md:px-0 max-w-4xl mx-auto">
{post.cover_image ? (
<img
src={post.cover_image}
alt={post.title}
className="aspect-[16/9] w-full rounded-2xl bg-gray-100 object-cover sm:aspect-[2/1] lg:aspect-[3/2]"
/>
) : null}
<div className="prose prose-lg prose-slate dark:prose-invert prose-headings:font-display font-default focus:outline-none max-w-full">
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }}></div>
</div>
</div>
);
} catch (error) {
return notFound();
}
}
Notice how we are awaiting on the anonGetPublishedBlogPostBySlug
function to fetch the blog post data. This function is a server action which is defined in src/data/anon/internalBlog.ts
. And since the BlogPostPage
is a server component, we can await on the data fetching function.
Streaming rendering
Server components have the added benefit of streaming rendering. Consider the page responsible for rendering the blog list page at /blog
. This page has to fetch two sets of data: the list of blog posts and the list of tags. We can fetch both of these data sets in parallel and stream the rendering of the page as soon as the data is available.
In this case, we have moved the data fetching into two separate components: Tags
and BlogList
.
Both of these components are server components and since they are wrapped in Suspense they can be rendered in parallel. Also, the page does not have to wait for the data to be fetched before it starts rendering. It can start rendering and render the child components as soon as the data is available.
import { T } from '@/components/ui/Typography';
import { PublicBlogList } from '../PublicBlogList';
import { TagsNav } from '../TagsNav';
import { Suspense } from 'react';
import {
anonGetAllBlogTags,
anonGetPublishedBlogPosts,
} from '@/data/anon/internalBlog';
export const metadata = {
title: 'Blog List | Nextbase',
description: 'Collection of the latest blog posts from the team at Nextbase',
icons: {
icon: '/images/logo-black-main.ico',
},
};
async function Tags() {
const tags = await anonGetAllBlogTags();
return <TagsNav tags={tags} />;
}
async function BlogList() {
const blogPosts = await anonGetPublishedBlogPosts();
return <PublicBlogList blogPosts={blogPosts} />;
}
export default async function BlogListPage() {
return (
<div className="space-y-8 w-full">
<div className="flex items-center flex-col space-y-4">
<div className="space-y-3 mb-6 text-center">
<T.Subtle>Blog</T.Subtle>
<T.H1>All blog posts</T.H1>
<T.P className="text-xl leading-[30px] text-muted-foreground">
Here is a collection of the latest blog posts from the team at
Nextbase.
</T.P>
</div>
<Suspense fallback={<T.Subtle>Loading tags...</T.Subtle>}>
<Tags />
</Suspense>
</div>
<Suspense fallback={<T.Subtle>Loading posts...</T.Subtle>}>
<BlogList />
</Suspense>
</div>
);
}
So the user experience for this page looks like this:
- the page starts rendering
- "All blog posts" title is rendered
- "Loading tags..." is rendered and when the tags are fetched, the tags are rendered
- "Loading posts..." is rendered and when the posts are fetched, the posts are rendered
This is a much better user experience than having to wait for the page to render until all the data is fetched.
Data fetching in client components
We don't fetching data much in client components unless in very rare cases. To fetch data in client components simply rely on useQuery
from
react-query. For eg: the login button in the navigation menu is a client component and it fetches the user data using useQuery
:
'use client';
import { supabaseUserClientComponentClient } from '@/supabase-clients/user/supabaseUserClientComponentClient';
import { useQuery } from '@tanstack/react-query';
export function LoginCTAButton() {
const { data: isLoggedIn, isFetching } = useQuery(
['isLoggedInHome'],
async () => {
const response = await supabaseUserClientComponentClient.auth.getUser();
return Boolean(response.data.user?.id);
},
{
// options
},
);
// if logged in , show dashboard button else show login button
// ...
}