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:

next.config.js
/** @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:

app/layout.tsx
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;

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:

app/head.tsx
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.

app/loading.tsx
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:

lib/api.ts
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:

app/page.tsx
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:

shared/SearchBar.tsx
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.

search/page.tsx
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:

lib/api.ts
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:

app/story/[id]/page.tsx
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:

app/story/loading.tsx
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!



Read more about Tutorials

Cover Image for Building an AI Writer SaaS with Next.js and Supabase

Building an AI Writer SaaS with Next.js and Supabase

ยท57 min read
Learn how to build an AI Writer SaaS with Next.js and Supabase - from writing SEO optimized blog posts to managing subscriptions and billing.
Cover Image for Announcing the Data Loader SDK for Supabase

Announcing the Data Loader SDK for Supabase

ยท8 min read
We're excited to announce the Data Loader SDK for Supabase. It's a declarative, type-safe set of utilities to load data into your Supabase database that you can use in your Next.js or Remix apps.
Cover Image for Adding AI capabilities to your Next.js SaaS with Supabase and HuggingFace

Adding AI capabilities to your Next.js SaaS with Supabase and HuggingFace

ยท20 min read
In this tutorial, we will learn how to use add AI capabilities to your SaaS using Supabase Vector, HuggingFace models and Next.js Server Components.
Cover Image for Building an AI-powered Blog with Next.js and WordPress

Building an AI-powered Blog with Next.js and WordPress

ยท17 min read
Learn how to build a blog with Next.js 13 and WordPress and how to leverage AI to generate content.
Cover Image for Using Supabase Vault to store secrets

Using Supabase Vault to store secrets

ยท6 min read
Supabase Vault is a Postgres extension that allows you to store secrets in your database. This is a great way to store API keys, tokens, and other sensitive information. In this tutorial, we'll use Supabase Vault to store our API keys
Cover Image for Introduction to Next.js Server Actions

Introduction to Next.js Server Actions

ยท9 min read
Next.js Server Actions are a new feature introduced in Next.js 13 that allows you to run server code without having to create an API endpoint. In this article, we'll learn how to use them.