React 第4周笔记useEffect +异步数据获取

路线图阶段:第一阶段(React 基础)第4周 项目:小相册(从 Picsum API 拉图片) 核心:学会”在组件里安全地做副作用”——尤其是从 API 取数据 心智模型延续:UI = f(state),但有些事(发请求、操作 DOM)不属于”算 UI”,要单独处理


1. 为什么需要 useEffect:副作用(Side Effect)

组件函数的本职工作是”根据 state 算出 UI 并返回”,这个过程应该是纯粹的。 但有些操作和”算 UI”无关、又必须做:

这些叫副作用(side effect)。它们不能直接写在组件函数体里。

反面教材:直接在组件体里 fetch → 无限循环

function Gallery() {
  const [photos, setPhotos] = useState([]);

  fetch("...").then(r => r.json()).then(data => setPhotos(data)); // ❌

  return <div>{photos.length}</div>;
}

为什么死循环:

渲染 → 执行 fetch → setPhotos → 触发重渲染 → 又执行 fetch → 又 setPhotos → ∞

根本原因:副作用(fetch)不该和”渲染”绑在一起,每次渲染都跑就出事。


2. useEffect 基本结构

import { useState, useEffect } from "react";

useEffect(() => {
  // 副作用写在这里
}, []); // ← 依赖数组(dependency array)

useEffect 把副作用从”渲染过程”里隔离出来,让你控制它什么时候跑。


3. 依赖数组(Dependency Array):副作用的”监视清单”

依赖数组告诉 React:只有清单里的值变了,才重新执行 effect。

useEffect(() => { ... }, []);
// 空清单 → 没有要监视的东西 → 只在挂载时跑一次(最常用于初次取数据)

useEffect(() => { ... }, [query]);
// 监视 query → 每次 query 变了才重新跑(如搜索关键词变了重新请求)

useEffect(() => { ... });
// 不写依赖数组 → 每次渲染都跑 → 这就是上面那个死循环的来源

推论:[query] 的 effect,如果 query 没变,即使组件因为别的 state 重渲染了很多次,也不会执行。 这正是依赖数组的价值:精确控制副作用何时跑,而不是每次渲染都跑。

类比设计:依赖数组像 Figma 的”响应式约束”——设定”当父容器宽度变化时才重新布局”, 依赖数组就是那个”当 X 变化时才执行”的条件。


4. 异步基础:Promise / async / await

(这部分是 JS 基础,不是 React;路线图第7-10周会系统学,这里够用即可)

fetch 是异步的

网络请求发出去不会立刻有结果,要等服务器回应。 fetch 不直接返回数据,而是返回一个 Promise(承诺)——“将来会给你结果”的占位符。

类比:便利店点现做便当,先给你一个”取餐号”(Promise),做好了再叫你。

Promise 的两层(易混点)

async function loadPhotos() {
  const res = await fetch("...");   // 第一层:fetch 的 Promise,被 await 拆开
  const data = await res.json();    // 第一层:res.json() 的 Promise,被 await 拆开
}
const result = loadPhotos();        // 第二层:async 函数整体,调用时又返回一个 Promise

.then() 链 vs async/await(等价)

// .then() 写法
fetch("...")
  .then(res => res.json())   // res 是"信封",res.json() 拆开成数据(也是异步,所以要第二个 then)
  .then(data => setPhotos(data));

// async/await 写法(更直观,读起来像同步)
async function loadPhotos() {
  const res = await fetch("...");
  const data = await res.json();
  setPhotos(data);
}

一条链的流程

发请求 → 拿到信封(res) → 拆信封变数据(data) → 用数据

5. useEffect 里调用 async 函数的正确姿势

❌ 不能直接把 async 函数传给 useEffect

useEffect(loadPhotos, []); // ❌ async 函数返回 Promise,违反 useEffect 约定,报警告

useEffect 的第一个参数要么不返回东西,要么返回一个”清理函数”。 async 函数总返回 Promise → 违反约定。

✅ 在 useEffect 内部定义并调用(主流写法)

useEffect(() => {
  async function loadPhotos() {
    const res = await fetch("...");
    const data = await res.json();
    setPhotos(data);
  }
  loadPhotos(); // 定义完立刻调用
}, []);

外层是普通箭头函数(返回 undefined,不返回 Promise),内部跑 async 逻辑。


6. 三状态:loading / error / data

真实 API 请求有三种状态,都要呈现给用户(只给空白页体验差)。

const [photos, setPhotos] = useState([]);
const [loading, setLoading] = useState(true);  // 初始 true:组件一挂载就要请求
const [error, setError] = useState(null);

loading 的判断逻辑(一句话)

开始等之前 = true,结果到手(成功或失败)= false。

挂载 → loading=true(显示"加载中")
     → 发请求(等待中,仍 true)
     → 结果回来:成功 setLoading(false) / 失败也 setLoading(false)
     → loading=false(显示照片或错误)

⚠️ 成功和失败都要 setLoading(false)。 只在成功时设,一旦请求失败,loading 永远卡在 true → 用户永远看到”加载中” → 高频 bug。

⚠️ 结束加载用 setLoading(false),不要用 setLoading(!loading)!loading 是”翻转”,语义是”来回切”(用于主题切换那种);这里意图是”明确设成 false”。 即使 !true 碰巧等于 false,也要写准确意图。


7. 渲染列表的两个坑(复习)

map 用 {} 必须 return

// ❌ 用 {} 却没 return → 每项返回 undefined → 一张图都不显示
{photos.map((photo) => {
  <div key={photo.id}>...</div>;
})}

// ✅ 用 () 自动返回
{photos.map((photo) => (
  <div key={photo.id}>...</div>
))}

// ✅ 或用 {} + 手动 return
{photos.map((photo) => {
  return <div key={photo.id}>...</div>;
})}

error 是对象,不能直接渲染

catch (error) {
  setError(error.message); // ✅ 存字符串(.message),不要存整个 Error 对象
}
// JSX 里直接 {error}(对象)会报错

8. Picsum API 实用要点


import { useState, useEffect } from "react";

function Gallery() {
  const [photos, setPhotos] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function loadPhotos() {
      try {
        const res = await fetch("https://picsum.photos/v2/list?page=1&limit=10");
        const data = await res.json();
        setPhotos(data);
        setLoading(false);
      } catch (error) {
        setError(error.message);
        setLoading(false);
      }
    }
    loadPhotos();
  }, []);

  if (loading) return <p>加载中...</p>;
  if (error) return <p>出错了:{error}</p>;

  return (
    <div style={{
      display: "grid",
      gridTemplateColumns: "repeat(3, 1fr)",
      gap: "12px",
    }}>
      {photos.map((photo) => (
        <div key={photo.id}>
          <img
            src={`https://picsum.photos/id/${photo.id}/300/200`}
            alt={photo.author}
            style={{ width: "100%", display: "block", borderRadius: "8px" }}
          />
        </div>
      ))}
    </div>
  );
}

function App() {
  return <Gallery />;
}

export default App;

第4周验收