導入
実際のアプリでは、画面を開いた瞬間にはデータがなく、サーバーに問い合わせて届くまで少し時間がかかります。この「まだ読み込み中」「取得に失敗した」「取得できた」という3つの状態を、そのまま state として持つのが定番のパターンです。この学習環境では本物のネットワーク通信ができないので、setTimeout で「時間がかかる疑似API」を再現しますが、パターンそのものは実際の fetch を使うときとまったく同じです。
説明
sequenceDiagram participant U as ユーザー操作 participant C as コンポーネント participant A as 疑似API U->>C: idを変更 C->>C: status を "loading" に C->>A: fakeFetchUser(id) A-->>C: 成功 または 失敗 C->>C: status を "success" / "error" に
// 本物の fetch の代わりに、時間がかかる疑似APIを用意する
function fakeFetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id <= 0) {
reject(new Error("不正なIDです"));
} else {
resolve({ id, name: `ユーザー${id}` });
}
}, 800);
});
}
function App() {
const [id, setId] = React.useState(1);
const [user, setUser] = React.useState(null);
const [status, setStatus] = React.useState("loading"); // "loading" | "success" | "error"
React.useEffect(() => {
let ignore = false; // 古い応答を無視するためのフラグ
setStatus("loading");
fakeFetchUser(id)
.then((data) => {
if (!ignore) {
setUser(data);
setStatus("success");
}
})
.catch(() => {
if (!ignore) setStatus("error");
});
return () => {
ignore = true;
};
}, [id]);
return (
<div style={{ fontFamily: "system-ui", maxWidth: 320 }}>
<button onClick={() => setId((n) => n + 1)}>次のユーザー</button>{" "}
<button onClick={() => setId(-1)}>不正なIDを試す</button>
{status === "loading" && <p>読み込み中...</p>}
{status === "error" && <p style={{ color: "crimson" }}>読み込みに失敗しました</p>}
{status === "success" && <p>{user.name}(id: {user.id})</p>}
</div>
);
}
statusを"loading" / "success" / "error"の3値で持ち、JSX 側は{status === "..." && ...}で出し分けます。これが「取得中の画面が真っ白/固まったように見える」問題を防ぐ基本形です。useEffectの依存配列に[id]を入れているので、idが変わるたびに取得し直します。let ignore = falseとクリーンアップでのignore = true…idを素早く連打すると、古いリクエストの応答が新しい応答より後に届くことがあります。これを無視しないと、画面が古いデータで上書きされてしまいます(競合状態、race condition と呼ばれるバグです)。- 実務では
fakeFetchUser(id)の部分がfetch("/api/users/" + id).then(r => r.json())に変わるだけで、考え方は同じです。
やってみよう
「次のユーザー」を連打して、途中の読み込みが飛ばされて最新の結果だけが表示されることを確認しましょう。「不正なIDを試す」を押してエラー表示になることも確かめてください。