Nextjs,Panda CSSで APP Router使ったニュースアプリを作成
はじめに
Nextjs v13.4 から新しく App Router 機能が実装されました。
ニュースアプリを作り APP Router を学んでいきましょう!
間違っている箇所がありましたらご指摘ください。
デモサイトです
デモサイト
準備
まずは環境構築です。
// npmの場合
npx create-next-app --ts
// yarnの場合
yarn create next-app --typescript
いくつか対話式の質問がありますが、任意の回答をしてください。
Would you like to use App Router? (recommended)
… No / Yes
と App Router 使いますか?と質問されるので
その質問は Yes と回答してください。
Would you like to use Tailwind CSS? › No / Yes
Tailwind CSS を使いますかと質問されるので。
そこは No と言ってください。
続いて Panda CSS をインストールします。
// npmの場合
npm install -D @pandacss/dev
// yarnの場合
yarn add --dev @pandacss/dev
続いて設定ファイルを作成します。
npx panda init --postcss
package.json を更新します。
"scripts": {
+ "prepare": "panda codegen",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"prepare": "panda codegen"を追加することにより
新しくパッケージを追加した際に
自動的にstyled-stystem
フォルダに
各ファイル設定が作成されます。
次にsrc/app
フォルダにあるglobal.css
を開いて
全て消してから次のコードを入力します。
@layer reset, base, tokens, recipes, utilities;
これで pandaCSS を使えるようになりました。
NewsAPI の取得
まずは NewsAPI の取得を行います。
公式サイト
Get API Key から名前,Email,パスワードを入力し、
You are...の部分は個人開発の場合は上の
I am an individual にチェックを入れましょう。
アカウントを作成し、APIKEY をゲットしたらまず
プロジェクトのディレクトリ直下に.env.local
ファイルを作成し、
そちらに入力します。
API_KEY=あなたのAPIをここに入れる
記事を出力
app ファルダ直下にある page.tsx に
データを取得するコードを書いていきます。
page Router では getStaticProps を用いて
データを取得してきましたが、
App Router ではasync/await
を用いて
データを取得します。
export default function Home() {
const API = process.env.API_KEY;
const res = await fetch(
`https://newsapi.org/v2/top-headlines?country=jp&pageSize=5&apiKey=${API}`
);
const json = await res.json();
const articles = json?.articles;
console.log(articles);
return <p>News</p>;
}
従来の Page Router の方法ですと
export default async function Home(props) {
console.log(props.articles);
return <p>News</p>;
}
export const getStaticProps = async () => {
const API = process.env.API_KEY;
const res = await fetch(
`https://newsapi.org/v2/top-headlines?country=jp&pageSize=5&apiKey=${API}`
);
const json = await res.json();
const articles = json?.articles;
return {
props: {
articles,
}
}
}
となるのでコードの記述量がだいぶ減りました。
軽く解説すると
res
関数に非同期として fetch を用いてデータを取得します。
json
関数では非同期で取得したデータを json 形式に変換しています。
最後にarticles
関数で json データにある記事データを
articles だけで出力できるようにしています。
動作の確認の為ローカル環境を立ち上げます。
// npmの場合
npm run dev
// yarnの場合
yarn dev
ターミナルに記事が表示されれば成功です
続いて API で取得した記事を表示します。
app ディレクトリ内に component フォルダを作成し、
article ファイルを作成します。
import { FC } from "react";
type Props = {
articles: [
article: {
author: string;
title: string;
publishedAt: string;
url: string;
urlToImage: string;
}
];
};
export const Articles: FC<Props> = ({ articles }) => {
return (
<section>
<div>
<h1>News App</h1>
</div>
{articles.map((article) => {
const time = new Date(article.publishedAt).toLocaleString();
return (
<a href={article.url} key={article.title}>
<article>
<div>
<h2>{article.title}</h2>
<p>{time}</p>
</div>
{article.urlToImage && (
// eslint-disable-next-line @next/next/no-img-element
<img
key={article.title}
src={article.urlToImage}
alt={`${article.title} image`}
/>
)}
</article>
</a>
);
})}
</section>
);
};
解説していきます。
type Props = {
articles: [
article: {
author: string;
title: string;
publishedAt: string;
url: string;
urlToImage: string;
}
];
};
まずはProps
として必要な型を定義していきます。
型を定義するのになにが必要かは先程
ターミナルに出力された部分を参考に必要な
部分の型を定義していきます。
とても字が汚いです。ごめんなさい。
export const Articles: FC<Props> = ({ articles }) => {
return (
<section>
<div>
<h1>News App</h1>
</div>
{articles.map((article) => {
const time = new Date(article.publishedAt).toLocaleString();
return (
...
)
})}
</section>
)
}
Articles という関数コンポーネントを定義しており
React の FC をコンポーネントの型とし
プロパティとして先程のProps
の型を入れます。
分割代入として props からarticles
を抽出します。
section の中に map 関数をに用いて
引数はarticle
として記事を1つ1つ表示します。
const time = new Date(article.publishedAt).toLocaleString();
こちらのコードは記事の中にある投稿時間を日本時間に置き換えています。
{
article.urlToImage && (
// eslint-disable-next-line @next/next/no-img-element
<img
key={article.title}
src={article.urlToImage}
alt={`${article.title} image`}
/>
);
}
このコードは記事の画像がある場合のみ
表示されるようにしています。
通常 Nextjs ではimg
タグを使用すると
Image
タグを推奨してるよ!と警告が出ます
今回なぜ
img
タグなのかというと
Nextjs の Image タグは外部の URL から画像を読み込む場合
そのドメインをnext.config.js
に記入しなくてはなりません。
今回の News API は世界中で 100 サイト以上の外部サイトから
ニュースを取得してきている為、
日本の必要なサイトだけでもいくつドメインを
記入しなくてはならないのかわからない為
今回はimg
タグを使用しています。
作成した Article コンポーネントを page.tsx に反映させます。
+ import { Articles } from "./components/articles";
export default function Home() {
const API = process.env.API_KEY;
const res = await fetch(
`https://newsapi.org/v2/top-headlines?country=jp&pageSize=5&apiKey=${API}`
);
const json = await res.json();
const articles = json?.articles;
- console.log(articles);
- return <p>News</p>;
+ return <Articles articles={articles} />;
}
これでローカル環境を立ち上げると
記事一覧が表示されます。
スタイルを設定していないので不格好です、
これからスタイルを書いていきます。
スタイルを書く
Panda CSS を用いてスタイルを書いていきます
Panda CSS とは
公式サイト上記公式サイトでも書いてある通り
ゼロランタイム CSS-in-JS ライブラリとなっております。従来のようにランタイム CSS in JS を利用することが
推奨されなくなってきている中、
ゼロインタイムを実現した Panda CSS が
リリースされました。
<section>
<div
+ className={css({
+ display: "flex",
+ justifyContent: "space-between",
+ margin: "5",
+ })}
>
<h1
+ className={css({
+ fontSize: "3xl",
+ fontWeight: "bold",
+ })}
>
News App
</h1>
</div>
{articles.map((article) => {
const time = new Date(article.publishedAt).toLocaleString();
return (
<a href={article.url} key={article.title}>
<article
+ className={css({
+ display: "flex",
+ flexDirection: "row-reverse",
+ justifyContent: "start",
+ padding: "12px",
+ borderTop: "1px solid #f0f0f0",
+ })}
>
<div
+ className={css({
+ paddingLeft: "20px",
+ })}
>
<h2>{article.title}</h2>
<p>{time}</p>
</div>
{article.urlToImage && (
// eslint-disable-next-line @next/next/no-img-element
<img
+ className={css({
+ width: "80px",
+ height: "80px",
+ })}
key={article.title}
src={article.urlToImage}
alt={`${article.title} image`}
/>
)}
</article>
</a>
);
})}
</section>
簡易的ではありますが、スタイルを当てました。
Header作成
続いてヘッダーを作成して、ニュースのカテゴリ毎に
切り替えられるようにします。
componentsフォルダーの中にheader.tsxファイルを作成します。
中身を書いていきます。
import Link from "next/link";
import { FC } from "react";
const TOPICS = [
{
path: "/",
title: "Top stories",
},
{
path: "/topics/business",
title: "Business",
},
{
path: "/topics/technology",
title: "Techonology",
},
{
path: "/topics/entertainment",
title: "Entertainment",
},
{
path: "/topics/sports",
title: "Sports",
},
{
path: "/topics/science",
title: "Science",
},
{
path: "/topics/general",
title: "general",
},
];
export const Header: FC = () => {
return (
<section>
<ul>
{TOPICS.map((topic, index) => (
<li key={index}>
<Link href={`${topic.path}`}>
<span>{topic.title}</span>
</Link>
</li>
))}
</ul>
</section>
);
};
TOPICSという関数にそれぞれカテゴリーとページリンクを
記述し、map関数を用いて展開しています。
こちらもスタイルを整えていきます。
import Link from "next/link";
import { FC } from "react";
+ import { css } from "../../../styled-system/css";
...
export const Header: FC = () => {
return (
- <section>
+ <section className={css({
+ height:"56px",
+ backgroundColor: "#93C5FD",
+ })}>
- <ul>
+ <ul className={css({
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ gap: "32px",
+ height: "100%",
+ })}>
{TOPICS.map((topic, index) => (
<li key={index}>
<Link href={`${topic.path}`}>
<span>{topic.title}</span>
</Link>
</li>
))}
</ul>
</section>
);
};
appディレクトリ直下のlayout.tsxにHeaderを追加します。
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
+ <Header/>
+ <main>
{children}
+ </main>
</body>
</html>
)
}
動的ルーティング
この状態だとまだそれぞれの
カテゴリーに飛ぶことができません。
動的ルーティングを用いてページを飛ばします。
動的ページを作る際はPage Routerと
同じく[]の中にページ名を入れることで
動的ルーティングになります。
appディレクトリ直下にtopicsフォルダを作成し、
さらにその中に[id]フォルダを作成します。
page.tsxファイルを作成しコードを書いていきます。
import { Article } from "@/app/_components/article";
type Props = {
id: string;
}
async function getPost(params: Props) {
const API = process.env.NEWS_API;
const topicRes = await fetch(
`https://newsapi.org/v2/top-headlines?country=jp&category=${params.id}&country=jp&apiKey=${API}`
);
const topicJson = await topicRes.json();
const topicArticles = await topicJson.articles;
return topicArticles;
}
async function Topic({ params }: {params:Props}) {
const title = await getPost(params);
return (
<div>
<Article articles={title} />
</div>
);
}
export default Topic;
解説していきます。
本来動的ルーティングを行う時は
getStaticPath
とgetStaticProps
が
必要でしたが、
App Routerからは不必要になります。
async function getPost(params: Props) {
const API = process.env.NEWS_API;
const topicRes = await fetch(
`https://newsapi.org/v2/top-headlines?country=jp&category=${params.id}&country=jp&apiKey=${API}`
);
const topicJson = await topicRes.json();
const topicArticles = await topicJson.articles;
return topicArticles;
}
getPost関数では引数にparamsを指定し、
newsAPIから記事を取得しています。
params.idという情報を使って特定のカテゴリーの
ニュースを取得しています。
async function Topic({ params }: {params:Props}) {
const title = await getPost(params);
return (
<div>
<Article articles={title} />
</div>
);
}
最後にTopic関数ですが、
先程のgetPost関数をtitleという関数に入れます。
Article関数にタイトルと記事一覧を表示させます。
先程のtitleを指定することにより、
カテゴリー毎の記事一覧を取得することができています。
最後に
おつかれさまでした!
これにてニュースアプリの完成です!
APP Routerを用いたアプリ開発の
お役に立てれば幸いです!
最後までお読みいただきありがとうございました。
Discussion