🧑💻
【React】Qiitaのクローンを作ってみよう(記事一覧画面)
取り組み
Reactの実案件に挑みましたが全然理解できていないので、わからないまま進めてしまった実案件の復習を兼ねて、Qiitaのクローンを作ってみようかと思いました。
chatGPT先生に聞いたところ、まずは初級レベルの記事一覧画面から進めます。
コンポーネント
まずはどこをコンポーネント化するか考えてみます。
他で使い回す可能性のあるものはコンポーネント化しておきます。(私こういうの考えるの苦手)
ここ(赤枠)のカードは使い回しそうですね。

こんな感じにしてみよう。
カードのコンポーネント。
import { Link } from "react-router"
export type ArticleCardProps = {
id: string
title: string
createdAt: string
updatedAt?: string
user: UserProps
}
export type UserProps = {
id: string
name: string
icon: string
}
export function ArticleCard({ id, title, createdAt, updatedAt, user }: ArticleCardProps) {
return (
<>
<div className="grid grid-cols-[32px_1fr] grid-rows-2 gap-x-2">
<span className="row-span-2 block w-8 h-8 rounded-full overflow-hidden">
<img src={user.icon} alt={user.name} width={32} height={32} />
</span>
<p className="text-sm">{user.name}</p>
<p className="text-xs text-[rgba(0,0,0,0.6)]">
<time dateTime={updatedAt ?? createdAt}>
{updatedAt
? `更新日:${new Date(updatedAt).toLocaleDateString("ja-JP")}`
: `作成日:${new Date(createdAt).toLocaleDateString("ja-JP")}`}
</time>
</p>
</div>
<div className="mt-2 ml-10">
<h3 className="text-xl font-bold">
<Link to={`/articles/${id}`}>{title}</Link>
</h3>
</div>
</>
)
}
そして、これをmapメソッドで配列を繰り返し処理する。
リストのコンポーネント。(これは検索結果でも使えそう)
import { ArticleCard } from "@/components/blocks"
import type { ArticleListProps } from "./ArticleListProps"
export type ArticleListItem = ArticleCardProps
export type ArticleListProps = {
items: ArticleListItem[]
}
export function ArticleList({ items }: ArticleListProps) {
return (
<ul className="mt-2 space-y-6">
{items.map(item => (
<li key={item.id} className="bg-white py-4 px-6 rounded-xl">
<ArticleCard {...item} />
</li>
))}
</ul>
)
}
ロジックを考える
ロジックはこうかな。
import type { ArticleCardProps } from "@/components/blocks"
import { useEffect, useState } from "react"
// Qiita APIのURL
const QIITA_API_ITEMS_URL = "https://qiita.com/api/v2/items"
// 1ページあたり表示件数のデフォルトは20件
export function useArticle(perPage: number = 20) {
// 記事の配列を管理するstate
const [items, setItems] = useState<ArticleCardProps[]>([])
// 読み込み中かどうかを管理するstate
const [loading, setLoading] = useState(true)
// エラー情報を管理するstate
const [error, setError] = useState<string | null>(null)
// コンポーネントがマウントされた時、またはperPageが変わった時にAPIを呼ぶ
useEffect(() => {
// 非同期関数でAPI呼び出し
async function fetchArticles() {
try {
// Qiita APIを呼び出す
const response = await fetch(`${QIITA_API_ITEMS_URL}?page=1&per_page=${perPage}`, {
headers: {
// アクセストークンをAuthorizationヘッダーに設定
Authorization: `Bearer ${import.meta.env.VITE_QIITA_TOKEN}`,
},
})
// レスポンスが失敗(ステータスコードが200系以外)の場合はエラーを投げる
if (!response.ok) {
throw new Error(`Qiita API 取得エラー:${response.status}`)
}
// JSONをパース
const data = await response.json()
// APIのレスポンスをArticleCardProps型に変換
const mapped: ArticleCardProps[] = data.map((item: any) => ({
id: item.id,
title: item.title,
createdAt: item.created_at,
updatedAt: item.updated_at,
url: item.url,
user: {
id: item.user.id,
name: item.user.name,
icon: item.user.profile_image_url,
},
}))
// stateにセットしてコンポーネントを再レンダリング
setItems(mapped)
} catch (e) {
// エラー発生時はerror stateにセット
setError(e instanceof Error ? e.message : "不明なエラー")
} finally {
// 成功・失敗に関わらずloadingをfalseに
setLoading(false)
}
}
// fetchArticlesを呼び出す
fetchArticles()
}, [perPage])
return { items, loading, error }
}
ざっくりの説明。
非同期通信でQiitaのAPIを呼び出し、発行したアクセストークンを設定して、返ってきたJSON形式をJavaScriptのオブジェクトに変換(パース)して、必要な型を整形(マッピング)して、Reactのstateに保存。
Qiita API アクセストークン発行方法
https://qiita.com/maiamea/items/680cca06f7825595cba0
ページ表示
ここまで準備した記事一覧を表示させてみる。
import { ArticleList } from "@/components/blocks"
import { useArticle } from "./hooks/use-article"
export function Home() {
const { items, loading, error } = useArticle(20)
if (loading) return <div>読み込み中…</div>
if (error) return <div>エラー:{error}</div>
return (
<div className="max-w-[1040px] mx-auto">
<h2 className="font-bold">記事一覧</h2>
<ArticleList items={items} />
</div>
)
}
おわり
今回ひとまずトップページに表示させてみました。
ArticleCardとArticleListは一つのファイルでもよかったかなぁ。でも冗長になりそう…
Discussion