Ivan
Posted on March 26, 2023
A beginner's guide to exploring the new Next.js app directory and server components
I've been avoiding trying the new app folder, mostly because I just recently got the hang of Next.js server-side functions, and was a little upset that they were introducing something new. However, in this field, it's important to adapt and be open to learning new things, especially in the fast-moving JavaScript ecosystem.
For those of you who are not familiar, Next.js has introduced a new app directory with a new layout and conventions. Although they haven't said it will replace the pages folder (the previous way of creating routes), it seems likely that it will.
To get a better understanding of how the new layout works and see if I like it, I decided to create a simple Pokémon app.
Let's get started
Run the following command to create a new Next.js app:
npx create-next-app@latest --typescript next-app-folder-pokemon
Make sure to respond "yes" to all the prompts, especially the one that asks if you want to use the experimental app directory.
Now navigate to the next-app-folder-pokemon
directory
Let's add the TypeScript types for Pokémon, just to ensure that TypeScript doesn't cause any issues later.
src/types/index.ts
export interface Pokemon {
id: number;
name: string;
height: number;
weight: number;
abilities: {
ability: {
name: string;
};
}[];
sprites: {
front_default: string;
};
types: {
type: {
name: Types;
};
}[];
stats: {
base_stat: number;
stat: {
name: string;
};
}[];
}
interface PokemonFromList {
name: string;
url: string;
}
export interface Pokemons {
count: number;
next: string;
previous: string;
results: PokemonFromList[];
}
type Types =
| "normal"
| "fighting"
| "flying"
| "poison"
| "ground"
| "rock"
| "bug"
| "ghost"
| "steel"
| "fire"
| "water"
| "grass"
| "electric"
| "psychic"
| "ice"
| "dragon"
| "dark"
| "fairy";
Furthermore, we need to specify the image hostname (for the Pokemon images) in the next.config.js
file to enable the use of Next's Image component to display images.
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true,
},
// here
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'raw.githubusercontent.com',
},
],
},
}
module.exports = nextConfig
Next, let's create a services folder inside the src directory. Here, we will add a pokemon.ts file, which will contain the two fetching functions we'll use in this app.
src/services/pokemon.ts
import { Pokemon, Pokemons } from "@/types";
const POKEMON_API = "https://pokeapi.co/api/v2";
export async function getPokemon(id: string): Promise<Pokemon> {
const response = await fetch(`${POKEMON_API}/pokemon/${id}`);
const data = await response.json();
return data;
}
export async function getPokemons(): Promise<Pokemons> {
// only fetch the first 151 pokemons
const response = await fetch(`${POKEMON_API}/pokemon?limit=151&offset=0`);
const data = await response.json();
return data;
}
Server Components
The app directory includes a remarkable feature of server components, allowing developers to build React components that can function on the server, similar to traditional server-side rendering. This feature not only provides enhanced performance but also offers greater flexibility. Although I can only provide a brief summary here, you can explore this topic further by watching this informative talk.
What I found particularly noteworthy about server components is that they provide greater flexibility when it comes to data fetching. With server components, developers can await the fetch function directly within the component, eliminating the need for the useEffect
hook to fetch data. This can simplify the code and make it easier to manage data fetching within components.
Let me show you.
Modify the src/app/page.tsx
with the following content
import Image from "next/image";
import Link from "next/link";
import { getPokemons } from "@/services/pokemon";
const SPRITE_URL =
"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon";
export default async function Page() {
// this magic here
const results = await getPokemons();
return (
<div>
<div>
{results.results?.map((pokemon, index) => (
<Link href={`/pokemon/${pokemon.name}`} key={pokemon.name}>
<Image
alt={pokemon.name}
width={100}
height={100}
src={`${SPRITE_URL}/${index + 1}.png`}
/>
<p>{pokemon.name}</p>
</Link>
))}
</div>
</div>
);
}
This page displays a list of Pokemon that can be clicked to navigate to their respective page, which we will create shortly.
In the new app directory in Next.js, dynamic routes work similarly to how they did in the previous pages folder. To create a dynamic route, we need to create a folder with square brackets and the name of the parameter we want to use. For example, to create a dynamic route using the slug
parameter, we would create a folder named [slug]
in the pages
directory. The most important file in this context is the page.tsx
file, which defines anything imported from it as a page. While Next.js also provides layout and template files for more advanced customization, we won't be using them in this tutorial.
To create a pokemon/:slug
route, create the following folders and file inside the app directory pokemon/[slug]/page.tsx
, then add the following content inside the file:
import Link from "next/link";
import Image from "next/image";
import { getPokemon, getPokemons } from "@/services/pokemon";
type Params = {
params: {
slug: string;
};
};
export default async function Pokemon({ params }: Params) {
const { slug } = params;
// magic strikes again
const pokemon = await getPokemon(slug);
const sprite = `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/${pokemon.id}.svg`;
return (
<div>
<Link href="/">Home</Link>
<h1>{pokemon?.name}</h1>
<Image
width={400}
height={400}
src={sprite}
alt={pokemon?.name}
priority
/>
</div>
);
}
export async function generateStaticParams() {
const res = await getPokemons();
return res.results.map((pokemon) => ({
slug: pokemon.name,
}));
}
In this code block, we see the implementation of the Pokemon page that is linked to the previous page. It imports the necessary components from Next.js, as well as the getPokemon
and getPokemons
functions from the pokemon
service.
The Pokemon
function takes in a parameter object with a slug
property, which is used to fetch the data of the specific Pokemon. The fetched data is then used to display the Pokemon's name and image on the page. The image URL is constructed using the Pokemon's ID and a pre-defined image URL template.
One notable difference from previous code examples is the addition of the generateStaticParams
function. This function is a replacement for the getStaticPaths
function and is responsible for generating the parameters (in this case, slugs) required for generating all the Pokemon pages statically.
Overall, the Pokemon
component is relatively straightforward, utilizing the power of server components to fetch and display the required data for each Pokemon page.
Metadata
Previously, I often used the next-seo
library to handle my metadata needs, although Next.js also provides a <Head/>
component for this purpose. However, the new app folder has some changes to how metadata is handled.
For static metadata, we can use the metadata
object.
Let's see an example of this in the src/app/page.tsx file:
// at the end of the file
export const metadata = {
title: "Trying the new app folder",
description: "Pokemon app made with Next.js",
};
Since each Pokémon page should have unique metadata with its image and information, using static metadata won't suffice in src/app/pokemon/[slug]/page.tsx
. Instead, we can use the generateMetadata
function to create dynamic metadata for each page.
// need to add the Metada type import
import type { Metadata } from "next";
// at the end of the file
export async function generateMetadata({ params }: Params): Promise<Metadata> {
const pokemon = await getPokemon(params.slug);
return {
title: `${pokemon.name} | Next.js + Pokemon`,
description: `${pokemon.name} | Next.js + Pokemon`,
openGraph: {
title: `${pokemon.name} | Next.js + Pokemon`,
description: `${pokemon.name} | Next.js + Pokemon`,
images: [
{
url: pokemon.sprites.front_default,
width: 400,
height: 400,
alt: pokemon.name,
},
],
},
};
}
Although the app directory brings or enables more features in combination with React, I won't go over all of them in this article. This was just meant to document my experience trying this new layout that Next.js is introducing. However, feel free to explore and mention any features that you consider worth highlighting in the comments.
I also added some styles to the app and deployed the app so that you can see it live. You can check out the live site at https://pokemon-app-directory.vercel.app and view the repository at https://github.com/ivanms1/pokemon-app-directory.
Conclusion
Overall, I had a pleasant experience trying out the new app directory in Next.js. While I intended to experiment with the new layout, I found myself more intrigued by Server Components and their potential to simplify development. However, I do have some concerns about how quickly the ecosystem will adapt to this new feature. A quick search revealed that many libraries do not yet support Server Components, and some would need a structural change to support them.
Regarding the change from the pages directory to the app directory, I think it's a positive move. It was frustrating that every file created in the pages folder became a page, and I know many others had workarounds to create a more page-like folder structure. Nonetheless, I don't think there's anything strictly difficult that would make the migration a hassle. I didn't explore the templates and layout files in-depth, but I'm sure they would come in handy for a larger application.
That's all for now, thank you for reading! If you found this article helpful, please consider sharing it and giving it a like. Also, feel free to follow me for more articles in the future.
Posted on March 26, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.