iTranslated by AI
Building an Article Posting Site with Next.js 13 App Directory
A new feature called App Router has been added since Next.js 13. This is a layout system different from the traditional pages directory. App Router has the following characteristics:
- Routing: In the
pagesdirectory, page routing was determined by the file name. For example, a file namedpages/about.jscorresponds to the path/about. In App Router, the file corresponding to routing has a fixed name calledpage.js. The file corresponding to the path/aboutwould be namedapp/about/page.js. In addition topage.js, there are various special files such aslayout.jsfor shared layouts andloading.jsfor displaying loading UI. - Rendering: Components within the App Router are treated as Server Components by default. If you want to treat them as Client Components, you need to declare
"use client"at the top of the file. - Data Fetching: Traditional
getStaticPropsandgetServerSidePropscannot be used in App Router. Instead, you can fetch data usingasync/awaitin Server Components. Also, thefetchAPI is used to leverage caching and request deduplication when fetching data. - Caching: When fetching data using the
fetchAPI, Next.js HTTP caching is enabled by default. Also, due to client-side caching, extra requests do not occur during client-side navigation.
By using App Router, you can expect not only an intuitive layout system but also performance improvements. In this article, let's build a simple article posting site using the app directory and experience the features of the new functionality.
The completed code can be found in the following repository:
Preparation of the Development Environment
First, create a Next.js application. If you clone from the azukiazusa1/nextjs-app-dir-example repository, the backend API will already be prepared.
git clone https://github.com/azukiazusa1/nextjs-app-dir-example.git
If you want to create it yourself, run the following command:
npx create-next-app@latest
Install the packages and start the development environment with the following commands:
npm install
npm run dev
When you access http://localhost:3000/, the following screen will be displayed.

Overview of App Router
First, the following files exist in the App Router's initial state:
-
page.tsx: A file that defines the UI corresponding to the routing. -
layout.tsx: The root layout of the application. In addition to the navigation header used across all pages, it sets the<html>and<body>tags.
Editing the page.tsx file
Let's edit the page.tsx file and confirm that the display changes.
export default function Home() {
return (
<div>
<h1>新着記事</h1>
<ul>
<li>記事1</li>
<li>記事2</li>
<li>記事3</li>
</ul>
</div>
)
}
After editing the file, access http://localhost:3000/ to see that the display has changed as follows.

Next, let's create a page that displays article details when accessing a path like /articles/{slug}. Since the structure of the App Router is mapped to the URL path, create a directory called app/articles/[slug].
mkdir app/articles/[slug]
Create the file responsible for the UI of the created URL path with the name page.tsx.
touch app/articles/[slug]/page.tsx
Let's edit the app/articles/[slug]/page.tsx file as follows. To get the dynamic path value (slug), it receives params from the arguments.
export default function Article({ params }: { params: { slug: string } }) {
return (
<div>
<h1>記事の詳細</h1>
<p>記事のスラッグ: {params.slug}</p>
</div>
);
}
Accessing http://localhost:3000/articles/next-js-app-dir-tutorial confirms that the content of the file edited above is displayed.
![Changes in app/articles/[slug]/page.tsx reflected in the browser](https://storage.googleapis.com/zenn-user-upload/cfc8be5edf6e-20230114.png)
Editing the layout.tsx file
Let's also edit the Layout. The app/layout.tsx file is called the Root Layout. The Root Layout is applied to all pages. Since Next.js does not automatically generate <html> or <body> tags, they must be defined in the app/layout.tsx file.
import Link from "next/link";
export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
themeColor: "#ffffff",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<head />
<body>
<header>
<h1>
<Link href="/">ブログ</Link>
</h1>
<Link href="/articles/new">記事を書く</Link>
</header>
{children}
<footer>
<small>© 2023 azukiazusa</small>
</footer>
</body>
</html>
);
}
Access http://localhost:3000/ and http://localhost:3000/articles/next-js-app-dir-tutorial to confirm that the same layout is applied to both pages.


Introducing Chakra UI
The application created so far is a bit plain, so we will introduce Chakra UI for styling. Chakra UI is a UI library available for React. It is characterized by offering many UI components and having excellent customizability.
First, install Chakra UI.
npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6
Setting up the Provider
To use Chakra UI, you need to set ChakraProvider at the root of the application. In App Router, app/layout.tsx becomes the root element. Let's set ChakraProvider here.
import Link from "next/link";
+ import { ChakraProvider } from "@chakra-ui/react";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<head />
<body>
+ <ChakraProvider>
<header>
{/* ... */}
+ </ChakraProvider>
</body>
</html>
);
}
However, a compilation error will occur as it is.

This is because components within the App Router are treated as React Server Components by default.
Distinguishing Between Server Components and Client Components
Server Components are a mechanism for rendering components only on the server side. Server Components have the following benefits:
- No JavaScript is sent to the client.
- Access to databases or GraphQL endpoints can be performed closer to the source.
As a result, you can benefit from faster initial page loads and smaller client-side JavaScript bundle sizes. Generally, you should use them as Server Components by default.
However, Server Components have some limitations:
- They do not hold state, so hooks like
useStateorContextcannot be used. - Lifecycle hooks like
useEffectcannot be used. - APIs available only in the browser, such as
localStorage, cannot be used. - Event handlers like
onClickoronChangecannot be used.
Components that manage state using useState or perform interactive actions using event handlers must be treated as Client Components. To treat a component within the App Router as a Client Component, declare "use client" at the top of the file.
Since Server Components and Client Components complement each other's strengths and weaknesses, it is necessary to use them appropriately.
<ChakraProvider> uses useState internally, so it cannot be treated as a Server Component. This is the cause of the compilation error.
To resolve this issue, you need to configure <ChakraProvider> to be treated as a Client Component instead of a Server Component.
To treat third-party components like <ChakraProvider> as Client Components, wrap them in a file that declares "use client". Let's create an app/Provider.tsx file.
"use client";
import { ChakraProvider } from "@chakra-ui/react";
export default function Provider({ children }: { children: React.ReactNode }) {
return <ChakraProvider>{children}</ChakraProvider>;
}
Then, replace <ChakraProvider> with <Provider> in app/layout.tsx.
import Link from "next/link";
- import { ChakraProvider } from "@chakra-ui/react";
+ import Provider from "./Provider";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<head />
<body>
- <ChakraProvider>
+ <Provider>
<header>
{/* ... */}
- </ChakraProvider>
+ </Provider>
</body>
</html>
);
}
This resolves the compilation error, and the Next.js application should now start normally.
Since all Chakra UI components depend on <ChakraProvider>, they only work in Client Components. Therefore, when using UI components, you must wrap them and declare "use client".
Declaring "use client" every time you use a Chakra UI component can be tedious. Let's export Chakra UI components together in app/common/components/index.tsx so they can be used as Client Components.
"use client";
export * from "@chakra-ui/react";
Now, when you want to use a Chakra UI component, you can write:
import { Button } from "./common/components";
This way, you don't have to worry about "use client".
Creating the Header Component
Now, let's create a common header component using Chakra UI. In App Router, unlike the traditional pages directory, you can freely place files as long as you don't use special filenames like page.tsx. Since we want to place the header component near the layout.tsx file, we will create Header.tsx under the App Router.
import { Box, Flex, Heading, Button } from "./common/components";
import NextLink from "next/link";
export default function Header() {
return (
<Box as="header">
<Flex
bg="white"
color="gray.600"
minH={"60px"}
py={{ base: 2 }}
px={{ base: 4 }}
borderBottom={1}
borderStyle="solid"
borderColor="gray.200"
align="center"
>
<Flex flex={1} justify="space-between" maxW="5xl" mx="auto">
<Heading as="h1" size="lg">
<NextLink href="/">Blog App</NextLink>
</Heading>
<Button
as={NextLink}
fontSize="sm"
fontWeight={600}
color="white"
bg="orange.400"
href="/articles/new"
_hover={{
bg: "orange.300",
}}
>
記事を書く
</Button>
</Flex>
</Flex>
</Box>
);
}
Similarly, create app/Main.tsx and app/Footer.tsx.
import { Container } from "./common/components";
export default function Main({ children }: { children: React.ReactNode }) {
return (
<Container
as="main"
maxW="container.lg"
my="4"
minH="calc(100vh - 115px - 2rem)"
>
{children}
</Container>
);
}
import { Container, Box, Text } from "./common/components";
export default function Footer() {
return (
<Box bg="gray.50" color="gray.700" as="footer">
<Container maxW="5xl" py={4}>
<Text as="small">© 2023 azukiazusa</Text>
</Container>
</Box>
);
}
Then, place <Header>, <Main>, and <Footer> in app/layout.tsx.
import Link from "next/link";
import Provider from "./Provider";
+ import Header from "./Header";
+ import Main from "./Main";
+ import Footer from "./Footer";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<head />
<body>
<Provider>
+ <Header />
+ <Main>{children}</Main>
+ <Footer />
</Provider>
</body>
</html>
);
}
Now the basic layout is complete. Let's confirm that it's displayed as expected.

Displaying the List of Articles
Let's fetch the list of articles from the API and display them on the top page. The API is already prepared in the pages/api/ directory.
In Next.js, data fetching is commonly performed on the server side. However, getServerSideProps and getStaticProps provided in traditional Next.js are not supported in App Router. Instead, fetching data from the API is done using async/await within a Server Component. Server Components also function as asynchronous components.
Data fetching in App Router fundamentally uses the Fetch API. While the Fetch API is a native feature of Web APIs, it is extended as follows when used in Next.js:
- Automatically deduplicates requests
- Requests called before dynamic functions (
cookies(),headers(),useSearchParams()) are HTTP cached by default - Supports
revalidateas a proprietary caching strategy
While data fetching can also be performed in Client Components, it is recommended to always do it within Server Components for the following reasons:
- Direct access to backend resources such as databases
- Sensitive information like access tokens is not exposed to the client
- Data fetching and rendering happen in the same environment, reducing client-server communication and work on the client's main thread
- Multiple data fetches can be performed in a single request
- Fetching data closer to the data source reduces latency
Now, let's actually perform data fetching in a Server Component. First, we'll prepare the type definitions. Create an app/types.ts file.
export type Article = {
id: number;
title: string;
content: string;
slug: string;
createdAt: string;
updatedAt: string;
};
export type Comment = {
id: number;
body: string;
articleId: number;
createdAt: string;
updatedAt: string;
author: Author;
};
export type Author = {
name: string;
avatarUrl: string;
};
In app/page.tsx, fetch the list of articles from http://localhost:3000/api/articles.
import type { Article } from "./types";
async function getArticles() {
const res = await fetch("http://localhost:3000/api/articles");
// Error handling is recommended
if (!res.ok) {
throw new Error("Failed to fetch articles");
}
const data = await res.json();
return data.articles as Article[];
}
export default async function Home() {
const articles = await getArticles();
return (
<div>
<h1>新着記事</h1>
<ul>
{articles.map((article) => (
<li key={article.id}>{article.title}</li>
))}
</ul>
</div>
);
}
The logic inside getArticles is the same as the typical usage of fetch. If an exception is thrown within the component, an error screen will be displayed by error.tsx, which will be described later.
By using async/await in the Home component, you can fetch and use data in a natural flow.
Data Caching
Let's also consider caching for data fetching. By default, using fetch automatically caches the data after it is fetched. This means the fetch options are set to { cache: "force-cache" } by default. The force-cache option works similarly to getStaticProps.
Since we are fetching a list of new articles here, data updates might occur frequently. Therefore, we will configure it to perform a request every time instead of caching the data.
To fetch new data every time fetch is executed, set cache: "no-store". no-store works similarly to getServerSideProps.
const res = await fetch("http://localhost:3000/api/articles", {
cache: "no-store",
});
Loading UI
A delay of 1500ms has been set for fetching the list of new articles. It is not user-friendly for nothing to be displayed during that time, including the header section which is unrelated to the data fetching. Therefore, let's try displaying a loading UI while data is being fetched.
In Next.js 13, a special file called loading.tsx within the App Router handles the role of displaying the loading UI. loading.tsx is displayed while data is being fetched on the server (i.e., until the Server Component's Promise is resolved), and it shows the new content once rendering is complete. This behavior is the same as the fallback in Suspense.
The concept looks something like this:
<html lang="ja">
<head />
<body>
<Provider>
<Header />
<Main>
<Suspense fallback={<Loading />}>
{/* page.tsx content is inserted into children */}
{children}
</Suspense>
</Main>
<Footer />
</Provider>
</body>
</html>
Create app/loading.tsx as follows:
import { Box, Spinner } from "./common/components";
export default function Loading() {
return (
<Box justifyContent="center" display="flex">
<Spinner color="orange.400" size="xl" />
</Box>
);
}
With loading.tsx, the loading UI is now displayed until the list of articles is fetched. Since loading.tsx is placed to wrap the page.tsx in the same directory, layouts such as the header are displayed immediately.

Error Handling
If an exception is thrown within a Server Component, the content of error.tsx is displayed. error.tsx wraps the page.tsx file in the same directory with an Error Boundary.
The error.tsx file works conceptually like this:
<html lang="ja">
<head />
<body>
<Provider>
<Header />
<Main>
<ErrorBoundary fallback={<Error />}>
{/* page.tsx content is inserted into children */}
{children}
</Suspense>
</Main>
<Footer />
</Provider>
</body>
</html>
The Error component receives the following Props:
-
error: The thrown exception object -
reset: A function to re-render the component where the exception occurred
Also, error.tsx must always be a Client Component.
"use client"; // Error components must be Client components
import { useEffect } from "react";
import { Heading, Button } from "./common/components";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div>
<Heading mb={4}>An unexpected error occurred.</Heading>
<Button onClick={() => reset()}>Try again</Button>
</div>
);
}
Let's check the error handling behavior by intentionally causing an exception in app/page.tsx.
async function getArticles() {
const res = await fetch("http://localhost:3000/api/articles", {
cache: "no-store",
});
+ throw new Error("Failed to fetch articles");

ArticleList Component
Finally, let's create a component responsible for displaying articles to tidy up the appearance. First, create the ArticleCard component.
import {
Card,
CardHeader,
CardBody,
CardFooter,
Heading,
Text,
} from "./common/components";
import NextLink from "next/link";
import { Article } from "./types";
export default function ArticleCard({ article }: { article: Article }) {
const formattedDate = new Date(article.createdAt).toLocaleDateString(
"ja-JP",
{
year: "numeric",
month: "long",
day: "numeric",
}
);
return (
<Card
as={"li"}
_hover={{
boxShadow: "xl",
}}
minW="100%"
>
<NextLink href={`/articles/${article.slug}`}>
<CardHeader>
<Heading size="md">{article.title}</Heading>
</CardHeader>
<CardBody>
<Text>{article.content.substring(0, 200)}...</Text>
</CardBody>
<CardFooter>
<Text fontSize="sm" color="gray.600">
{formattedDate}
</Text>
</CardFooter>
</NextLink>
</Card>
);
}
Create the ArticleList component to display the list of article cards.
import { VStack } from "./common/components";
import ArticleCard from "./ArticleCard";
import { Article } from "./types";
export default function ArticleList({ articles }: { articles: Article[] }) {
return (
<VStack spacing={4} as="ul">
{articles.map((article) => (
<ArticleCard key={article.id} article={article} />
))}
</VStack>
);
}
Incorporate the ArticleList component into app/page.tsx.
import ArticleList from "./ArticleList";
import { Heading } from "./common/components";
// ...
export default async function Home() {
const articles = await getArticles();
return (
<div>
<Heading as="h1" mb={4">
新着記事
</Heading>
<ArticleList articles={articles} />
</div>
);
}
Accessing http://localhost:3000 should display the list of articles as follows.

Article Detail Page
Let's create the article detail page. Here, we will implement functionality to fetch and display the body of the article along with comments for that article. A specific article is fetched from api/articles/{slug}, and comments for the article are fetched from api/articles/{slug}/comments.
Let's also consider the caching strategy for each. Since the article body is assumed not to be updated frequently, it can be cached for a certain period. The cache lifetime can be set by specifying next.revalidate in the fetch options. This is a feature similar to the traditional ISR.
On the other hand, it would be unnatural if comments are not reflected immediately after posting, so it is better not to use caching. Similar to fetching the list of articles, specify cache: "no-store" in the fetch options.
import { notFound } from "next/navigation";
import { Article, Comment } from "../../types";
const getArticle = async (slug: string) => {
const res = await fetch(`http://localhost:3000/api/articles/${slug}`, {
next: { revalidate: 60 },
});
if (res.status === 404) {
// Calling the notFound function displays not-found.tsx
notFound();
}
if (!res.ok) {
throw new Error("Failed to fetch article");
}
const data = await res.json();
return data as Article;
};
const getComments = async (slug: string) => {
const res = await fetch(
`http://localhost:3000/api/articles/${slug}/comments`,
{
cache: "no-store",
}
);
if (!res.ok) {
throw new Error("Failed to fetch comments");
}
const data = await res.json();
return data as Comment[];
};
If the API returns a 404 when fetching an article, the notFound function from next/navigation is called. When this function is called, the not-found.tsx in the nearest directory is displayed.
Let's also create not-found.tsx.
import { Heading, Button } from "../../common/components";
import NextLink from "next/link";
export default function NotFound() {
return (
<div>
<Heading mb={4}>The article you are looking for could not be found.</Heading>
<Button as={NextLink} href="/">
Back to Top
</Button>
</div>
);
}
When accessing the URL of a non-existent article, it is displayed as follows:

Let's return to displaying the article details. Call getArticle and getComments within the component to display the fetched data. When calling multiple APIs that have no dependencies on each other, it is recommended to use Promise.all so that the processes run in parallel.
export default async function ArticleDetail({
params,
}: {
params: { slug: string };
}) {
const articlePromise = getArticle(params.slug);
const commentsPromise = getComments(params.slug);
const [article, comments] = await Promise.all([
articlePromise,
commentsPromise,
]);
return (
<div>
<h1>{article.title}</h1>
<p>{article.content}</p>
<h2>Comments</h2>
<ul>
{comments.map((comment) => (
<li key={comment.id}>{comment.body}</li>
))}
</ul>
</div>
);
}
Now, when you visit the article detail page, the article content and the list of comments will be displayed. However, there is one issue: it takes too long to display the article.
A delay of 1000ms is set for fetching the article, and 3000ms for the list of comments. If only viewing the article body, it should originally be displayable in 1000ms. However, because Promise.all also waits for the completion of fetching the comment list, it takes 3000ms to display the article content.

The purpose of a user visiting the article detail page is to view the article body, and the list of comments is just supplementary information. Therefore, it is not desirable to wait for the completion of the comment list retrieval.
So, let's try using streaming for fetching the comment list. By using streaming, you can display the article body the moment the article retrieval is complete, without waiting for the comment list retrieval to finish.
Fetching Comments with Streaming
Streaming breaks the page's HTML into small chunks and sends them progressively to the client. This allows the page to start displaying parts of itself without waiting for all data to be fetched.
To control where streaming occurs, wrap the asynchronous component with <Suspense>. Split the part that fetches the comment list into a separate component and wrap it with <Suspense> as follows:
export default async function ArticleDetail({
params,
}: {
params: { slug: string };
}) {
const articlePromise = getArticle(params.slug);
const commentPromise = getComments(params.slug);
const article = await articlePromise;
return (
<div>
<h1>{article.title}</h1>
<p>{article.content}</p>
<h2>Comments</h2>
<Suspense fallback={<div>Loading comments...</div>}>
{/* @ts-expect-error Currently, TypeScript reports a type error if JSX returns a Promise, but this will be resolved in the future */}
<Comments commentPromise={commentPromise} />
</Suspense>
</div>
);
}
async function Comments({
commentPromise,
}: {
commentPromise: Promise<Comment[]>;
}) {
const comments = await commentPromise;
return (
<ul>
{comments.map((comment) => (
<li key={comment.id}>{comment.content}</li>
))}
</ul>
);
}
Now let's check the behavior. After about 1000ms, the article body is displayed, and during that time, "Loading comments" is shown for the comment list. Then, after another 2000ms, the list of comments is displayed.

<head> Tag
On the article detail page, you'll want to set the article title in the <title> tag for SEO purposes. To set <head> tags for each route, export a metadata object or a generateMetadata function from the page.tsx or layout.tsx file.
The contents of the configured metadata will be inserted into the <head /> tag of the root layout.
-
metadataobject: Set<head>tag contents statically -
generateMetadatafunction: Set<head>tag contents dynamically
The generateMetadata function receives params as an argument and can dynamically fetch values using async/await to set <head> in an object format. The second argument, parent, allows you to reference metadata set in parent directories.
import type { Metadata, ResolvingMetadata } from 'next';
export async function generateMetadata({
params,
}: {
params: { slug: string };
parent?: ResolvingMetadata;
}): Promise<Metadata> {
const article = await getArticle(params.slug);
return {
title: article?.title,
description: article?.content,
};
}
While it might seem inefficient because it sends the same request as the ArticleDetail component, Next.js automatically deduplicates requests using fetch, so it doesn't impact performance.
Metadata is evaluated in order from the segment closest to the root directory to the segment containing the page.tsx file. For example, in app/layout.tsx, it is written as follows:
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
themeColor: "#ffffff",
}
The title and description keys overlap with the metadata set in app/pages/articles/[slug]/page.tsx. In this case, the metadata set in the page.tsx closest to the page takes precedence, so the title and description for the article detail page will be the article's title and body. Furthermore, since themeColor is only set in app/layout.tsx, its content is inherited as is.
Styling
Finally, we apply styling using Chakra UI. Create the following components:
-
app/articles/[slug]/ArticleContent.tsx: Component that displays the article title and body -
app/articles/[slug]/Comments.tsx: Component that displays the list of comments -
app/articles/[slug]/LoadingComments.tsx: Component displayed while comments are loading
import {
Card,
CardHeader,
CardBody,
Text,
Heading,
} from "../../common/components";
import { Article } from "../../types";
export default function ArticleContent({ article }: { article: Article }) {
return (
<Card as="article">
<CardHeader>
<Heading as="h1">{article.title}</Heading>
</CardHeader>
<CardBody>
<Text as="p" fontSize="md">
{article.content}
</Text>
</CardBody>
</Card>
);
}
import {
Card,
CardBody,
StackDivider,
VStack,
Text,
Box,
Avatar,
Flex,
} from "../../common/components";
import { Comment } from "../../types";
export default async function Comments({
commentPromise,
}: {
commentPromise: Promise<Comment[]>;
}) {
const comments = await commentPromise;
if (comments.length === 0) {
return (
<Text as="p" fontSize="md">
No comments yet.
</Text>
);
}
return (
<VStack
divider={<StackDivider borderColor="gray.200" />}
spacing={4}
as="ul"
align="stretch"
px={4}
>
{comments.map((comment) => (
<CommentItem key={comment.id} comment={comment} />
))}
</VStack>
);
}
function CommentItem({ comment }: { comment: Comment }) {
return (
<Flex as="li" listStyleType="none" align="center">
<Avatar
size="sm"
name={comment.author.name}
src={comment.author.avatarUrl}
mr={4}
/>
<Text fontSize="sm">{comment.body}</Text>
</Flex>
);
}
import {
StackDivider,
VStack,
Flex,
SkeletonCircle,
Skeleton,
} from "../../common/components";
export default function LoadingComments({}) {
return (
<VStack
divider={<StackDivider borderColor="gray.200" />}
spacing={4}
as="ul"
align="stretch"
px={4}
>
<CommentSkeltonItem />
<CommentSkeltonItem />
<CommentSkeltonItem />
</VStack>
);
}
function CommentSkeltonItem() {
return (
<Flex as="li" listStyleType="none" align="center">
<SkeletonCircle size="8" mr={4} />
<Skeleton height="14px" width="60%" />
</Flex>
);
}
import ArticleContent from "./ArticleContent";
import Comments from "./Comments";
import { Heading } from "../../common/components";
import LoadingComments from "./LoadingComments";
const getArticle = async (slug: string) => {
// ...
}
const getComments = async (slug: string) => {
// ...
}
export default async function ArticleDetail({
params,
}: {
params: { slug: string };
}) {
const articlePromise = getArticle(params.slug);
const commentPromise = getComments(params.slug);
const article = await articlePromise;
return (
<div>
<ArticleContent article={article} />
<Heading as="h2" mt={8} mb={4}>
Comments
</Heading>
<Suspense fallback={<LoadingComments />}>
{/* @ts-expect-error Currently, TypeScript reports a type error if JSX returns a Promise, but this will be resolved in the future */}
<Comments commentPromise={commentPromise} />
</Suspense>
</div>
);
}
Ultimately, the display will look like this:

Creating an Article
Next, we will implement the article creation feature. Since we want to display a creation form at the path /articles/new, create a file named app/articles/new/page.tsx. We will implement it as a Client Component because it performs state management using a form.
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import {
Heading,
FormControl,
FormLabel,
Input,
Textarea,
Button,
} from "../../common/components";
export default function CreateArticle() {
const router = useRouter();
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
await fetch("/api/articles", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ title, content }),
});
setLoading(false);
router.push("/");
};
return (
<div>
<Heading mb={4}>Create Article</Heading>
<form onSubmit={handleSubmit}>
<FormControl>
<FormLabel>Title</FormLabel>
<Input value={title} onChange={(e) => setTitle(e.target.value)} />
<FormLabel>Body</FormLabel>
<Textarea
value={content}
onChange={(e) => setContent(e.target.value)}
/>
<Button
type="submit"
color="white"
bg="orange.400"
isLoading={loading || isPending}
mt={4}
>
Create
</Button>
</FormControl>
</form>
</div>
);
}
It displays a form to input the article title and content. When the create button is pressed, it calls the article creation API using fetch. Once the API call is complete, it navigates to the top page using router.push("/").
To use useRouter within the App Router, you need to import it from next/navigation instead of next/router.
As shown below, the form is displayed and you can now create articles.

However, there is one problem. After creating an article and navigating to the top page, the newly created article is not displayed.

This is because page transitions via router.push are "Soft Navigations". In Soft Navigation, if a cache for the destination exists, it is reused, and no new request is made to the server.
Since the cache for the top page exists, the old article list is being displayed. To invalidate the cache, you need to call router.refresh.
- import { useState } from "react";
+ import { useState, useTransition } from "react";
// ...
+ const [isPending, startTransition] = useTransition();
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
await fetch("/api/articles", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ title, content }),
});
setLoading(false);
router.push("/");
+ startTransition(() => {
+ router.refresh();
+ });
};
By calling router.refresh to invalidate the cache, new data is now fetched from the server when navigating to the top page.

Summary
We have reviewed the basic operations of the App Router. The fact that Server Components are the default is a major feature, and it feels like a welcome change that enables the creation of higher-performance applications.
Additionally, one of the key points of the App Router is the ability to group files related to a page within a single directory. This might suggest different approaches compared to traditional file configurations.
Discussion
とすると
というエラーがでました。
として実行しました。
おっしゃるとおり、
npm installが正しいコマンドでした🙇ご指摘ありがとうございます。記事の詳細ページの箇所で詰まっています。
app/articles/[slug]/page.tsx
このファイルはどうするのですか?
コメントありがとうございます。どのような事象が発生していて詰まっているのか、具体的に提示いただけると回答できそうです。
marlさんと同じ事象でしたので解決しました。
ありがとうございました。
初めまして。こちらの記事を見て勉強させていただいてます。


nextjs-app-dir-exampleをcloneさせていただいたのですが、詳細ページのコメントの表示がうまくいかず、エラーが返されてしまいます。
getComments関数の、
http://localhost:3000/api/articles/${slug}/commentsを見てみると[]しか表示されず、コメントがうまく引っ張って来れていないことが原因かなと思っているのですがこちらよろしければご教授いただきたいです><コメントありがとうございます。こちら私の環境で試してみましたが、再現しませんでしたので、どのようなエラーメッセージが表示されたかなどの情報を提示いただけると助かります。
一応、http://localhost:3000/api/articles/1428cbaa-64fe-d260-e1a2-07e6c6d19e76/comments が
[]を返すことは想定どうりです。この記事にはコメントが紐づいていないという想定ですので。お返事ありがとうございます!2番目の記事はコメントもともと紐づいてなかったです...!1つ目の記事のURLだとデータはうまく表示されました。失礼いたしました。
コメント部分( <Comments commentPromise={commentPromise} />)のところを 表示するとブラウザにerror.tsxのメッセージが表示されます。
また、Comments.tsx:36 Uncaught (in promise) TypeError: comments.map is not a function とエラーが出ており、commentsの中身が取得できていなさそうです...
app/articles/[slug]/page.tsx を作成し、そこに詳細ページのコードを置いています。
この記事の
app/pages/articles/[slug].tsxに書く方法だと404が表示されてしまったためです。お手数おかけしてしまい申し訳ありません。お手隙の際に見ていただけますとありがたいです。
詳細な情報のご提示ありがとうございます🙇
記事の中では
getCommentsの最後の行がreturn data.comments as Comment[]となっておりますが、正しくはreturn data as Comment[]でした。app/articles/[slug].tsxではなく、app/articles/[slug]/page.tsxとするのもおっしゃるとおりでした。ご報告誠にありがとうございます。ありがとうございます!

commentsも、articleと同じように
app/articles/[slug]/page.tsxのところでconst comments = await commentPromise;として、 commentsをpropsで受け渡したらコメントが表示されるようにはできました!ありがとうございました。
「ArticleList コンポーネントをapp/page.tsxに組み込みます。」
ここの段階でつまづいてしまっていて、どのように書けばいいか
おしえていただけると幸いです。。。
レポジトリにおいて完成後のコードを公開しておりますので、適宜ご参照ください。