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.
Top comments (8)
Hey can you explain when to use
interface
and when to usetype
?ChatGPT Conversation
Me:
When should I use a typescript interface vs type
GPT-4:
Both TypeScript interfaces and types (type aliases) are used to describe the shape of data, and they have some overlapping use cases. Choosing between them depends on your specific requirements and style preferences.
Here are some guidelines to help you decide when to use an interface or a type alias:
Use interfaces when:
You need to describe the shape of an object for class implementations. Interfaces are meant for defining contracts for classes.
You want to use declaration merging. Interfaces can be merged automatically if you declare an interface with the same name multiple times, effectively extending the interface.
You want a more traditional object-oriented programming approach with better IDE support for auto-completion and refactoring.
Use type aliases when:
You need to represent more complex type constructs like unions, intersections, mapped types, and conditional types. Type aliases offer more flexibility for creating these complex types.
You want to type alias primitive types, like strings or numbers, which is not possible with interfaces.
You want to create a type that can represent a function signature, tuple, or other non-object types.
Keep in mind that these guidelines are not strict rules, and you can use interfaces and type aliases interchangeably in many scenarios. However, by understanding their strengths and use cases, you can make more informed decisions about which to use in your TypeScript projects.
ME:
Can I just stick to interfaces if I wanted to?
GPT-4:
Yes, you can primarily use interfaces in your TypeScript projects if you prefer that style. Interfaces are a powerful way to define the shape of objects and create contracts for classes. However, you might still need to use type aliases in some cases, as they offer certain capabilities that interfaces don't have.
Here are some scenarios where you might need to use type aliases instead of interfaces:
Complex type constructs: If you need to represent union types, intersection types, mapped types, or conditional types, you'll need to use type aliases.
Non-object types: If you want to create a type for function signatures, tuples, or other non-object types, you'll need to use a type alias.
Type aliasing primitive types: If you want to create a type alias for a primitive type like string, number, or boolean, you'll need to use a type alias.
In most other situations, you can use interfaces to define the shape of your data. As long as you are aware of the limitations of interfaces and understand when type aliases might be necessary, you can choose to primarily use interfaces in your TypeScript projects.
Way to devalue a community. I'm sure most comments and even articles themselves could technically be replaced with a ChatGPT response but I feel that's missing the point of community.
I switched all my
interface
withtype
.Here's a quick video covering the differences:
youtube.com/watch?v=zM9UPcIyyhQ
Hi, Do you have any idea how to add a search functionality using redux toolkit, The state management on Nextjs 13 with app dir is kinda tricky.
The question would be why to use Redux at all (or mobx or any other global state management tool).
You'll have a
page
(A.K.A.view
outside Next world) which is effectively a Component plus you can keep a shared state in that view for all contained components, hence reducing the need to global state management to zero.Take a look at this situation:
This way you can set the logic inside
SearchBar
component to filter the original API response based on certain criteria, set the state up to the parent and it will propagate to ListResults, which will show the available filtered data.We sent the apiData to the SearchBar as well, so we can reset the results to "all available data" when the search input gets it's value removed by the user, or when all filters get cleared.
There are multiple ways to deal with this.
To set a couple of examples:
1- If the above example does't suit you, because it's just a frontend filtering and it does not grab new data from the API, you may want to set up a filtering object and use it to call to the micro-service again (please use debounce or throttle methodologies if searching on keyUp or things like that) each time a filter gets changed or an
apply filters
button gets pressed, then you just do it and update thesetSearchResults
with that new received data. You won't need to store the original data in this case.2- if you want to cache this information you may use a ServiceWorker to cache this information and read from it, while updating it's contents every now and then in a background & async manner.
3- If heavy computational tasks need to take place to apply the filters, or new data needs to be gathered in real time, you can instead apply key-value for each filter in the queryString (queryParameters) and make the ServerSide (Node in the case of Next JS or any external service/micro-service) handle them by applying a navigation. This is commonly used when applying first category filters. e.g.
-
myDomain/posts
// shows all posts-
myDomain/posts/myCategory1
// shows category 1 posts-
myDomain/posts/myCategory2
// shows category 2 postsIn this case you just provide "links" to those categories, no extra job required in frontend. The filtering will take place in the server (usually reading the
slug
, extracting the category to filter for, query-ing the DB based on this filter and return back the correct data).Best regards
Really tricky
Yeah, i agree with you!