R初めてのReact

Day 4useEffect と データ取得 — 外の世界とつながる

読み込み中・成功・失敗の3状態を設計する

データ取得には必ず「待ち」と「失敗」がある。loadingとerrorを状態として持ち、ユーザーに伝えるUIを作る。

取得には必ず3つの状態がある

前のレッスンではデータを取って表示するところまでやりました。でも実際のアプリでは、それだけでは不十分です。通信には時間がかかるので、データがまだ来ていない「読み込み中」の時間が必ずあります。さらに、サーバーが落ちていたり電波が悪かったりで「失敗」することもあります。つまりデータ取得は、読み込み中・成功・失敗という3つの状態を必ず通ります。この3つをきちんと扱うのが、実務の定番の考え方です。

たとえるなら、ネット通販で注文したときの状態と同じです。「配送中(まだ届いていない)」「到着(受け取れた)」「配送トラブル(届かなかった)」の3つがあり、アプリはそれぞれに応じた表示をするべきです。配送中なのに真っ白な画面を見せたら、ユーザーは壊れたのかと不安になります。だから、いま自分がどの状態にいるかを画面に出すことが大切です。

loadingとerrorをstateとして持つ

3状態を扱うには、データを入れる箱に加えて、読み込み中かどうかを表すloadingと、失敗した理由を入れるerrorという状態を用意します。取得を始めるときにloadingをtrueにし、成功したらデータを入れてloadingをfalseに、失敗したらerrorに理由を入れます。この3つのstateの組み合わせで、いまどの状態かが表現できます。

TSX
import { useEffect, useState } from "react";

function UserCard({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    setError(null);
    fetch(`/api/users/${userId}`)
      .then((r) => {
        if (!r.ok) throw new Error("取得に失敗しました");
        return r.json();
      })
      .then((data) => setUser(data))
      .catch((e) => setError(e.message))
      .finally(() => setLoading(false));
  }, [userId]);

  if (loading) return <p>読み込み中...</p>;
  if (error) return <p>エラー: {error}</p>;
  return <p>{user.name}</p>;
}
loading→成功/失敗の順に画面を分岐する定番パターン

response.okのチェックを忘れない

fetchはサーバーが404や500を返しても、通信自体が届いていれば「成功」として.then()に進んでしまいます。だからr.okを確認し、失敗ステータスなら自分でthrowしてcatchに送る必要があります。これを忘れると、エラーページのHTMLをデータだと思い込んで表示が崩れる、といった事故になります。

状態管理あり・なしを比べる

状態管理なし

TSX
function UserCard({ userId }) {
  const [user, setUser] = useState(null);
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then((r) => r.json())
      .then(setUser);
  }, [userId]);
  // 取得中は user が null で user.name が落ちる
  // 失敗しても永遠に空白のまま、原因もわからない
  return <p>{user.name}</p>;
}

状態管理あり

TSX
function UserCard({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  // ...fetchでloading/error/userを更新...
  if (loading) return <p>読み込み中...</p>;
  if (error) return <p>エラー: {error}</p>;
  return <p>{user.name}</p>;
}

状態管理なしの左側は、取得が終わる前はuserがnullなのにuser.nameを読もうとして画面が壊れます。失敗しても真っ白なまま何も起きず、ユーザーは原因がわかりません。右側のように3状態を分けると、待っている間は「読み込み中」を、失敗したら理由を、成功したら中身を、それぞれ確実に見せられます。この差が、動くだけのアプリと使えるアプリの分かれ目です。

なぜ3状態を意識するのか

ユーザーにとって最も不安なのは、何が起きているかわからない沈黙です。読み込み中と伝えれば待てますし、失敗と理由を伝えれば再試行できます。3状態の設計は技術というより、待たせている相手への配慮です。実務ではこの分岐が当たり前の作法として求められます。

AIにはこう聞く

「このデータ取得コンポーネントに、loading・error・successの3状態を持たせて。fetchのr.okチェックとcatch、finallyでのloading解除も入れて」と頼むと、実務的な形にしてくれます。生成後は、失敗時に本当にerrorへ入るか、loadingが必ずfalseに戻るかを確認しましょう。

読んでも腑に落ちない所は、AIに噛み砕いてもらいましょう。

理解度チェック

答えられれば、面接で説明できる状態に一歩近づきます。

Q1

データ取得で必ず考えるべき3つの状態はどれ?

Q2

fetchでサーバーが404を返したとき、そのままだと何が起きますか?

Q3

loadingとerrorの状態を持たないと、どんな不便が起きるか説明してください。

記述式です。まず自分の言葉で答えを考えてから、模範解答を開いて自己採点してください。