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:

src/app/(external-pages)/blog/([slug])/page.tsx
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.

src/app/(external-pages)/blog/(list)/page.tsx
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:

src/components/ui/NavigationMenu/ExternalNavbar/LoginCTAButton.tsx
'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
  // ...
}