この課題のねらい
ここは「ただ動くアプリ」から「ちゃんと気配りされたアプリ」に一段上げるところです。
テーマは UX(ユーザー体験)対応──つまり、
ローディング中に何も出さない“無言の時間”をなくす
エラーが起きたときに、ユーザーを置き去りにしない
失敗しても「もう一回試せる」逃げ道を用意する
この 3 つを、React の state と条件分岐でちゃんと表現していきます。
ベースとなる「データ取得コンポーネント」
まずは、前の「データ取得」の回で作ったようなベースを用意します。
"use client";
import { useEffect, useState } from "react";
type Post = {
id: number;
title: string;
};
export default function PostsPage() {
const [posts, setPosts] = useState<Post[] | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
async function fetchPosts() {
try {
setIsLoading(true);
setError(null);
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5");
if (!res.ok) {
throw new Error(`HTTPエラー: ${res.status}`);
}
const data: Post[] = await res.json();
setPosts(data);
} catch (err) {
console.error(err);
setError("データの取得に失敗しました。時間をおいて再度お試しください。");
} finally {
setIsLoading(false);
}
}
useEffect(() => {
fetchPosts();
}, []);
return (
<main style={{ padding: "24px", fontFamily: "sans-serif" }}>
{/* ここに UX 対応を足していく */}
</main>
);
}
TSXここに「ローディング表示」「エラー表示」「再取得ボタン」を肉付けしていきます。
必須課題① ローディング表示
「何も出ていない時間」をなくす
ユーザーからすると、「クリックしたのに何も起きてないように見える時間」が一番不安です。
そこで、isLoading が true の間は、はっきり「読み込み中」と伝えます。
if (isLoading) {
return (
<main style={{ padding: "24px", fontFamily: "sans-serif" }}>
<p>読み込み中です...</p>
</main>
);
}
TSXこのようにコンポーネントの先頭で早期 return してしまうと、
ローディング中は「読み込み中です…」だけ
読み込みが終わったら、下の通常表示に切り替わる
という分かりやすい挙動になります。
もう少し UX を上げるなら、簡単なスピナー風の見た目にしてもいいです。
if (isLoading) {
return (
<main style={{ padding: "24px", fontFamily: "sans-serif" }}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<span
style={{
width: "14px",
height: "14px",
borderRadius: "9999px",
border: "2px solid #e5e7eb",
borderTopColor: "#3b82f6",
animation: "spin 0.8s linear infinite",
}}
/>
<span>読み込み中です...</span>
</div>
</main>
);
}
TSX本番なら CSS でアニメーションを書くところですが、今は「ローディング中だと分かる表示」があれば十分です。
必須課題② エラー表示
「失敗したこと」と「どうしてほしいか」を伝える
エラーが起きたときに、ただ何も表示しないのは一番つらいです。error state にメッセージが入っているときは、それを画面に出します。
if (error) {
return (
<main style={{ padding: "24px", fontFamily: "sans-serif" }}>
<p style={{ color: "red", marginBottom: "8px" }}>{error}</p>
<p style={{ fontSize: "14px", color: "#6b7280" }}>
ネットワーク環境を確認して、しばらくしてから再度お試しください。
</p>
</main>
);
}
TSXここで意識してほしいのは、
「失敗しました」で終わらせない
「どうしてほしいか」を一言添える
ということです。
ユーザー目線で見ると、
何が起きたのか分からない
自分が何をすればいいのか分からない
この 2 つが一番ストレスなので、そこを潰してあげるイメージです。
通常時の表示(成功パターン)
ローディングでもエラーでもないときに、データを表示します。
if (isLoading) {
// ローディング表示(さっきのやつ)
}
if (error) {
// エラー表示(さっきのやつ)
}
return (
<main style={{ padding: "24px", fontFamily: "sans-serif" }}>
<h1>記事一覧</h1>
{posts && (
<ul style={{ marginTop: "12px" }}>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)}
</main>
);
TSXこの 3 段階の分岐が、「UX 対応されたデータ取得コンポーネント」の基本形です。
挑戦課題 再取得ボタン追加
「失敗したら終わり」ではなく「やり直せる」ようにする
ここからが UX の一歩踏み込んだところです。
エラーが出たときに、「再取得」ボタンを用意して、ユーザーが自分でリトライできるようにします。
さっきの fetchPosts 関数は、すでにコンポーネントの外側に切り出してあるので、それをそのままボタンから呼び出せます。
エラー表示部分をこう変えます。
if (error) {
return (
<main style={{ padding: "24px", fontFamily: "sans-serif" }}>
<p style={{ color: "red", marginBottom: "8px" }}>{error}</p>
<p style={{ fontSize: "14px", color: "#6b7280", marginBottom: "12px" }}>
ネットワーク環境を確認して、再度取得をお試しください。
</p>
<button
onClick={fetchPosts}
style={{
padding: "8px 16px",
borderRadius: "9999px",
border: "none",
backgroundColor: "#3b82f6",
color: "white",
cursor: "pointer",
fontSize: "14px",
}}
>
再取得する
</button>
</main>
);
}
TSXこれで、
取得に失敗する
エラー文と「再取得する」ボタンが出る
ボタンを押すと fetchPosts が再度実行される
という流れになります。
ローディング中の再取得ボタンの扱い
もう一歩だけ丁寧にするなら、「再取得中はボタンを無効化する」こともできます。
<button
onClick={fetchPosts}
disabled={isLoading}
style={{
padding: "8px 16px",
borderRadius: "9999px",
border: "none",
backgroundColor: isLoading ? "#9ca3af" : "#3b82f6",
color: "white",
cursor: isLoading ? "not-allowed" : "pointer",
fontSize: "14px",
}}
>
{isLoading ? "再取得中..." : "再取得する"}
</button>
TSXこうしておくと、
連打されて無駄に何回も fetch が走る
「押したのかどうか分からない」
といったストレスを減らせます。
まとめ:UX対応でつかんでほしいこと
この課題で本当に持ち帰ってほしいのは、次の感覚です。
ローディング中は「何もない時間」を作らず、はっきり「読み込み中」と伝える
エラー時は「失敗したこと」と「どうしてほしいか」をセットで出す
再取得ボタンを用意して、「失敗したら終わり」ではなく「やり直せる」体験にする
これができると、同じ「データ取得コンポーネント」でも、
ただ動くだけの画面
ちゃんとユーザーに寄り添った画面
の差が、はっきり出てきます。
