Kodokon kodokon.com

调用 API:fetch、加载与错误

在副作用中用 fetch 加载远程数据,并干净利落地对加载状态和错误状态进行建模。

10 分钟 · 3 题

在 Kodokon 中打开本课

一个网络请求既耗时又可能失败:你的界面必须反映这些现实。专业的惯例是追踪三份不同的状态:接收到的数据、一个加载中布尔值,以及一个可能出现的错误。请求放在 useEffect 内部,因为联系服务器是一个副作用。

JSX
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
  fetch("https://api.example.com/users")
    .then((res) => {
      if (!res.ok) throw new Error("HTTP " + res.status);
      return res.json();
    })
    .then((data) => setUsers(data))
    .catch((err) => setError(err.message))
    .finally(() => setLoading(false));
}, []);
一个请求对应三份状态:数据、加载中、错误。

在渲染中,按顺序处理这些情况:先是加载中,然后是错误,最后是数据。这些提前返回让代码保持可读,并保证用户总能看到某些连贯的东西:一个等待指示器、一条失败信息,或者预期的列表。

JSX
if (loading) return <p>Loading…</p>;
if (error) return <p>Error: {error}</p>;

return (
  <ul>
    {users.map((user) => (
      <li key={user.id}>{user.name}</li>
    ))}
  </ul>
);
提前返回:一次只显示一种情况。

还有最后一个陷阱:如果某个依赖项变化了(比如一个 userId),一个新请求就会发出去,而旧请求可能仍在途中。如果旧请求最后才响应,它就会覆盖掉新数据。经典的对策是:一个由清理函数设为 trueignore 标志,用来丢弃过时的响应。

JSX
useEffect(() => {
  let ignore = false;
  setLoading(true);

  fetch("/api/users/" + userId)
    .then((res) => res.json())
    .then((data) => {
      if (!ignore) setUser(data);
    })
    .finally(() => {
      if (!ignore) setLoading(false);
    });

  return () => {
    ignore = true;
  };
}, [userId]);
多亏了这个标志,每一个过时的响应都被丢弃。

知识检测

确认你已牢记本课的重点内容。

  1. 为什么要为一个请求追踪三份状态(数据、加载中、错误)?
    • 因为 fetch 需要这三个变量才能工作
    • 为了让界面反映每一个阶段:等待、失败或成功
    • 为了防止 React 在每次渲染时重新发起请求
  2. 你的 API 以 500 状态响应,但 catch 从未触发。为什么?
    • fetch 只在网络故障时拒绝 promise,而不是在 HTTP 错误状态时
    • catch 在 useEffect 内部无法工作
    • 服务器必须发送一个特殊的响应头才能触发 catch
  3. 在副作用的清理函数中被设为 true 的 ignore 标志,是用来做什么的?
    • 在服务器端取消正在进行的 HTTP 请求
    • 阻止 React 重新执行副作用
    • 丢弃一个已经变得过时的请求的响应