Getting Started with Next.js Server Components

A simple introduction to using Server Components and the new Layouts Folder Structure with Next.js 13

·8 min read
Cover Image for Getting Started with Next.js Server Components

Vercel has finally shipped Next.js 13, and with it one of the most requested features: Layouts. The new app structure is basically a new framework, full of new functionalities and possibilities that were simply impossible before.

As a result, Next 13 is easily the de-facto framework that React 18 envisioned when it was released, using the full power of Suspense and Server Components.

The changes and possibilities Next.js 13 brings are revolutionary: thanks to the new Server Components and seamless Suspense integration, Next. js applications will be smaller and faster.

Yet, the transition to using Server Components won't be the simplest: as I said above, the new Next.js 13 Layouts are basically a new framework within the old one.

In this blog post, I'm to write a step-by-step tutorial to build a Hacker News Clone using Next.js 13 and utilizing only the new Server Components Layouts.

This blog post is great for those who want to see a quick overview of how Server Components work with Next.js.

Are you ready? Let's start.

Getting Started

To get started, let's start a new Next.js application with create-next-app:

npx create-next-app --ts

After following the prompts, we open the new folder in the IDE.

To enable the new app directory, we add the experimental flag to the Next.js configuration file:

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  experimental: {
    appDir: true,
    enableUndici: true
  }
}

module.exports = nextConfig;

Additionally, we added the property enableUndici: true, which uses the undici package instead of node-fetch. The node-fetch package has a weird bug of not allowing payloads larger than 15kb. Weird.

Now, I'll simply delete everything inside the pages folder, with the exception of the api folder: Next.js requires the API functions to be placed within pages, for the time being.

So, are we deleting _app.tsx and _document.tsx? Yes. These are no longer needed when working with the new Next.js app directory. Gone. Done. Ciao.

This introduces lots of question marks:

  1. How do we pass data to a component?
  2. How do we initialize our applications?
  3. Where do we place our app-wide Providers?

Believe me, I was as confused as you are, and still am while writing this. But let's go on.

Tree Structure

The new folder tree structure adds some conventions that we need to use for handling specific states: for example, error boundaries, loading skeletons, the page's heading, etc.

This is how our folder structure will look like:

├── lib
  └── api.ts
  └── types.ts
├── app
    ├── shared
│   ├── ask
      └── page.tsx
│   ├── top
      └── page.tsx
│   ├── latest
      └── page.tsx
│   ├── latest
      └── search.tsx
│   ├── stories
      └── [id]
        └── page.tsx
        └── loading.tsx
│   ├── loading.tsx
│   ├── error.tsx
│   ├── not-found.tsx
│   ├── layout.tsx
│   ├── head.tsx

As you can see from the above, every actual page needs to be named page.tsx. If it's named anything but one of the special file names above, it's simply another file we can import. This improves colocation, so we can add our files close to where they're used.

Furthermore, you can see ewe added other "special" files:

  1. layout.tsx: this component is the base layout. It replaces both _app. tsx and _document.tsx.
  2. loading.tsx: this will render a component when our page is suspended. For example, while loading data from our Server Component.
  3. head.tsx: this will be the base head, which replaced the next/head component.
  4. not-found.tsx: this component will be rendered when hitting a 404
  5. error.tsx: this component will render when catching an uncaught exception.

Creating the Root Layout

All the Next.js applications using the new app folder structure need to define a root layout. This component will replace the existing _app.tsx and _document.tsx files.

To create the root layout, we need to add a layout.tsx file in the app folder: this is the root component that will be applied to every page that you add to your app folder.

In the case of our application, it will look like the below:

import '../styles/globals.css';

import Sidebar from './shared/Sidebar';

function RootLayout({ children }: React.PropsWithChildren) {
  return (
    <html lang={'en'}>
      <body>
        <div className={'flex'}>
          <div
            className={'w-2/12 max-w-xs dark:bg-black-400 bg-gray-50 h-screen'}
          >
            <Sidebar />
          </div>

          <div
            className={'bg-white dark:bg-black-500 w-full overflow-x-hidden'}
          >
            <div className={'relative mx-auto h-screen w-full overflow-y-auto'}>
              <div className={'p-6'}>{children}</div>
            </div>
          </div>
        </div>
      </body>
    </html>
  );
}

export default RootLayout;

Don't worry if you do not see the "Sidebar" component, you'll be able to see the details in the source code linked at the end of this article.

Creating the root Head

The special head.tsx component is meant to replace the Head component we import from next/head.

By adding the root head component, every page will automatically inject the head component defined below:

function Head() {
  return (
    <>
      <title>Next.js Layouts Demo</title>
    </>
  );
}

export default Head;

Creating a Loading spinner for the root layout

When the component is suspended, we can render a loading indicator just by adding layout.tsx to our root.

function Loading() {
  return (
    <ContentLoader
      width={1500}
      height={400}
      viewBox="0 0 1500 400"
      backgroundColor="#777"
      foregroundColor="#999"
    >
      {
        // more code here
      }
    </ContentLoader>
}

Adding Server Components Pages

Ok, now let's add our root page component. To add pages, you will use the convention page.tsx. The root page.tsx represents the home page of our application.

In this page will render the Hacker News front page items. So, we will loading the items using the function getFrontPage.

Let's define that first. To create our clone, we will be using the Hacker News Algolia API:

const BASE_URL = `https://hn.algolia.com/api/v1`;

export async function getFrontPage() {
  return fetch(`${BASE_URL}/search?tags=front_page`)
    .then((r) => r.json())
    .then((r) => r.hits);
}

Now, we import the function getFrontPage and we wrap it in the special (and not stable) React hook named use. Thanks to this hook, the component will suspend until the data is fetched.

As you can see from the below, we don't even need to use async/await:

import { use } from 'react';
import Heading from './shared/Heading';
import { getFrontPage } from '../lib/api';
import StoriesList from './shared/StoriesList';
import SearchBar from './shared/SearchBar';

function HomePage() {
  const stories = use(getFrontPage());

  return (
    <div className={'flex flex-col space-y-6'}>
      <SearchBar />

      <Heading>Front Page</Heading>

      <div>
        <StoriesList stories={stories} />
      </div>
    </div>
  );
}

export default HomePage;

Using Page Parameters

Let's assume we want to use a search bar, and we will display the results in the search/page.tsx page.

Let's create a simple search component: this component will submit a GET request to the /search page, and will append the input query as a query parameter:

import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';

function SearchBar(
  props: React.PropsWithChildren<{
    value?: string | undefined;
  }>
) {
  return (
    <form className={'w-full'} action={'/search'} method={'GET'}>
      <div
        className={
          'dark:bg-black-300 flex space-x-4 items-center transition-all dark:focus-in:bg-black-400 bg-gray-100 focus-in:bg-gray-200 rounded-lg px-4 py-3 w-full'
        }
      >
        <MagnifyingGlassIcon className={'h-5'} />

        <input
          autoComplete={'off'}
          className={'bg-transparent w-full dark:bg-black-300 outline-none'}
          name={'query'}
          defaultValue={props.value}
          type="text"
          placeholder={'Search...'}
        />
      </div>
    </form>
  );
}

export default SearchBar;

Now, we can create our search/page.tsx page that will read the query and submit a request to the HN API.

In the component below, we can also see how to redirect users without the ctx API we used to use using getServerSideProps.

import { use } from 'react';
import { redirect } from 'next/navigation';

import StoriesList from '../shared/StoriesList';
import { getResultsFromQuery } from '../../lib/api';
import SearchBar from '../shared/SearchBar';
import Heading from '../shared/Heading';

function SearchPage(props: { searchParams: Record<string, string> }) {
  const query = props.searchParams.query;

  if (!query) {
    return redirect('/');
  }

  const data = use(getResultsFromQuery({ query, page: 0 }));

  return (
    <div className={'flex flex-col space-y-6'}>
      <SearchBar value={query} />

      <Heading>
        {data.nbHits.toLocaleString()} Search Results found for &quot;{query}
        &quot;
      </Heading>

      <StoriesList stories={data.hits} />
    </div>
  );
}

export default SearchPage;

And below is the API function:

export async function getResultsFromQuery(params: {
  page: number;
  query: string;
}) {
  const queryParams = `tags=story&hitsPerPage=100&page=${params.page}&query=$
{params.query}`;

  return fetch(`${BASE_URL}/search?${queryParams}`).then((r) => r.json());
}

Using Page segment parameters

Finally, let's see an example to retrieve the page's segment parameters using the new Server Components. The page below will display the detailed data from a Hacker News story.

To add a dynamic segment, we will simply add a [id] folder, and add the page.tsx file within it:

import { use } from 'react';
import { redirect } from 'next/navigation';
import Link from 'next/link';
import { ArrowLeftIcon } from '@heroicons/react/24/outline';

import { getCommentsByStoryId, getItemById } from '../../../lib/api';
import Heading from '../../shared/Heading';
import CommentsSection from '../../shared/CommentsSection';
import { StoryItemChild } from '../../../lib/types';

function StoryPage(props: { params: Record<string, string> }) {
  const id = props.params.id;

  if (!id) {
    return redirect('/');
  }

  const item = use(getItemById(id));
  const comments = use(getCommentsByStoryId(id));

  return (
    <div>
      <div className={'flex flex-col space-y-4'}>
        <Link className={'hover:underline'} href={'../'}>
          <span className={'flex items-center text-sm font-medium space-x-2'}>
            <ArrowLeftIcon className={'h-4'} />
            <span>Back</span>
          </span>
        </Link>

        <div className={'flex flex-col space-y-2'}>
          <Heading>
            {item.url ? (
              <Link
                className={'hover:underline'}
                target={'_blank'}
                href={item.url}
              >
                {item.title}
              </Link>
            ) : (
              item.title
            )}
          </Heading>

          <div className={'text-sm text-gray-500 dark:text-gray-400'}>
            <p>
              {item.points} points by {item.author}
            </p>
          </div>
        </div>

        <div className={'max-w-2xl'}>
          <StoryChildrenRenderer items={item.children} />
        </div>

        <div>
          <CommentsSection comments={comments} />
        </div>
      </div>
    </div>
  );
}

function StoryChildrenRenderer(
  props: React.PropsWithChildren<{
    items: StoryItemChild[];
  }>
) {
  return (
    <div className={'flex flex-col space-y-4'}>
      {props.items.map((child) => {
        return (
          <div key={child.id}>
            <div dangerouslySetInnerHTML={{ __html: child.text }} />

            <StoryChildrenRenderer items={child.children ?? []} />
          </div>
        );
      })}
    </div>
  );
}

export default StoryPage;

Below we define the API function to fetch an item and its comments (which are not nested, for simplicity):

export async function getItemById(storyId: string): Promise<StoryItem> {
  return fetch(`${BASE_URL}/items/${storyId}`).then((r) => r.json());
}

export async function getCommentsByStoryId(
  storyId: string
): Promise<StoryComment[]> {
  return fetch(`${BASE_URL}/search?tags=comment,story_${storyId}`)
    .then((r) => r.json())
    .then((response) => response.hits);
}

As we want to display a different loading screen for the item's page, we can add a loading.tsx component within the story folder:

import { Facebook } from 'react-content-loader';

function Loading() {
  return (
    <Facebook
      width={1500}
      height={400}
      viewBox="0 0 1500 400"
      backgroundColor="#777"
      foregroundColor="#999"
    />
  );
}

export default Loading;

Source Code

You can grab the full source code on GitHub. Please follow the README to run the example locally. If you have any question, please shoot!


Stay informed with our latest resources for building a SaaS

Subscribe to our newsletter to receive updatesor

Read more about

Cover Image for Authenticating users with Remix and Supabase

Authenticating users with Remix and Supabase

·16 min read
Learn how to use Remix and Supabase to authenticate users in your application.
Cover Image for How Makerkit helps boost your SaaS SEO

How Makerkit helps boost your SaaS SEO

·4 min read
Learn how Makerkit can help boost your SaaS SEO thanks to its optimized codebase and SEO-friendly features.
Cover Image for How to sell code with Gumroad and Github

How to sell code with Gumroad and Github

·7 min read
Sell and monetize your code by giving private access to your Github repositories using Gumroad
Cover Image for Migrating to Next.js Server Components Layouts

Migrating to Next.js Server Components Layouts

·6 min read
A simple guide to migrating your _app.tsx component to the new Server Components released with Next.js 13
Cover Image for Counting a collection's documents with Firebase Firestore

Counting a collection's documents with Firebase Firestore

·2 min read
In this article, we learn how to count the number of documents in a Firestore collection using a custom React.js hook.
Cover Image for Pagination with React.js and Firebase Firestore

Pagination with React.js and Firebase Firestore

·6 min read
In this article, we learn how to paginate data fetched from Firebase Firestore with React.js