0%

已完成 0/29 关

教学模式

你可以随时调整。

此设置调整教师的教学方式,无需重新生成课程。

第 29 关 含代码 含表格 约 24 分钟

使用 Effect 与外部系统同步:渲染后取数据与清理机制

使用 Effect 与外部系统同步:渲染后取数据与清理机制

恭喜你来到了这门课程的最后一站! 回顾前面的旅程,你学会了如何用组件描述界面、如何用 Props 传递数据、如何用 State 响应点击并驱动列表渲染。

现在,我们来攻克最后一个、也是实际开发中最为关键的拼图: 当组件显示在屏幕上之后,如何自动去向服务器获取一次数据,并把数据展示给用户?

这就是 React 中最为著名的 useEffect Hook 的用武之地。


1. 什么是 Effect?它与事件有什么不同?

在深入代码之前,我们必须分清 React 组件里的两类逻辑:

  1. 渲染代码(Rendering code): 位于组件函数的顶层。它只负责读取 props 和 state,进行纯粹的计算,并返回 JSX。渲染代码必须是纯净的,绝不能在里面发送网络请求!
  2. 事件处理函数(Event handlers): 组件内部由特定用户动作(如点击按钮、输入表单)引发的函数。它们可以包含副作用(如提交购买表单)。

但是,有一种情况上述两者都无法解决: 如果某个操作,不是由用户的某一次具体点击引起的,而是只要组件出现在屏幕上,就必须自动执行呢? 比如:

  • 组件刚刚被打开,必须立即向后端获取商品详情;
  • 聊天室界面一展开,必须立即连接聊天服务器。

Effect 允许你指定由“渲染自身”引起的副作用,它会在 DOM 提交完成、页面呈现在屏幕上之后执行!


2. 编写 Effect 的三步法

第一步:声明 Effect

从 React 中导入 useEffect,并在组件顶层调用它:

import { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // 这里的代码会在组件渲染呈现在屏幕上之后运行!
  });

  return <div />;
}

第二步:指定 Effect 的依赖项(Dependencies)

默认情况下,如果没有传依赖项,Effect 会在每一次重新渲染后都执行。 但绝大多数时候,你并不希望它这么频繁地运行。比如获取数据,你通常只希望在刚打开时拉取一次。

我们通过向 useEffect 传入第二个参数——依赖项数组(Dependency Array),来精确控制它的触发频率:

依赖项写法执行时机与行为
不写依赖项 useEffect(() => {})每次渲染完成后都会执行一遍
空依赖项数组 useEffect(() => {}, [])仅在组件初次挂载(出现)在屏幕上时执行一次! 后续重新渲染绝不再跑
指定依赖变量 useEffect(() => {}, [userId])在初次挂载时执行一次,以及当 userId 发生改变重新渲染后才再次执行

第三步:添加清理函数(Cleanup Function)

某些 Effect 会启动一些需要被撤销的操作(比如打开了 WebSocket 连接、启动了定时器、或者发起了异步请求)。 如果需要清理,让你的 Effect 函数返回一个清理函数:

useEffect(() => {
  // 建立连接...
  return () => {
    // 断开连接,在组件销毁或下一次 Effect 执行前执行!
  };
}, []);

3. 核心实战:在操作或挂载之后安全获取数据并展示

现在,我们来实现课程最终目标的核心能力:使用 Effect 获取一次数据并更新到页面上。

在 React 中使用 Effect 获取数据,有一个极其经典且必须规避的陷阱——竞态条件(Race Condition): 假设用户飞快地先点击了“查看用户 A”,又点击了“查看用户 B”:

  • 针对 A 的请求先发出,但网络延迟了 3 秒;
  • 针对 B 的请求后发出,但网络很快,1 秒就返回了;
  • 页面先显示了 B;但 2 秒后,A 的旧数据才姗姗来迟返回,如果不加控制,它就会把 B 的最新数据覆盖掉!

官方推荐的标准解法:使用 ignore 标记

在清理函数中,把一个布尔值 ignore 设为 true:

import { useState, useEffect } from 'react';

export default function UserProfile({ userId }) {
  const [person, setPerson] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let ignore = false; // 1. 声明一个忽略标记
    setLoading(true);

    async function startFetching() {
      // 模拟向远程 API 获取数据
      const data = await fetchUserData(userId);
      // 2. 如果组件在数据返回前已经被切换或销毁,忽略这次过期的数据
      if (!ignore) {
        setPerson(data);
        setLoading(false);
      }
    }

    startFetching();

    // 3. 清理函数:当 userId 改变或组件卸载时触发
    return () => {
      ignore = true;
    };
  }, [userId]); // 仅在 userId 改变时重新拉取!

  if (loading) {
    return <p>正在飞速加载用户信息...</p>;
  }

  return (
    <div className="profile-card">
      <h2>{person.name}</h2>
      <p>职业: {person.title}</p>
      <p>简介: {person.bio}</p>
    </div>
  );
}

// 模拟异步网络接口
function fetchUserData(id) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({
        id: id,
        name: id === '1' ? '居里夫人' : '爱因斯坦',
        title: id === '1' ? '物理学与化学先锋' : '理论物理学家',
        bio: id === '1' ? '两度荣获诺贝尔奖的伟大科学家。' : '提出相对论,重塑了人类的时空观。'
      });
    }, 800);
  });
}

发生了什么?

  1. 当 userId 传入时,组件初次渲染显示“正在飞速加载…”;
  2. 渲染提交后,useEffect 启动,异步拉取对应数据;
  3. 数据返回时,ignore 依然是 false,调用 setPerson(data) 触发重新渲染,卡片信息亮起!
  4. 如果中途 userId 突然切换,上一次 Effect 的清理函数立即执行将 ignore 设为 true,旧网络请求即便稍后返回也不会产生任何污染!

4. 全课程终极融会贯通:打造你的完整小界面

现在,让我们把整门课程学过的所有积木,拼成一个真正完整的 React 小界面!

要求包含:

  • 几个独立的组件(App、InputArea、ArticleList、ProfileCard);
  • 使用 Props 传递数据;
  • 点击按钮更新 State;
  • 在列表中渲染一项一项的内容(map + key);
  • 使用 Effect 在挂载后获取一次数据并展示到页面上。
import { useState, useEffect } from 'react';

// 组件 1:负责数据展示的作者名片(展示 Effect 异步获取的数据)
function AuthorCard() {
  const [author, setAuthor] = useState(null);

  useEffect(() => {
    let ignore = false;
    async function loadAuthor() {
      // 模拟在挂载后取一次数据
      const data = await new Promise(r => setTimeout(() => r({
        name: 'React 研习者',
        badge: '全栈筑基达成'
      }), 500));
      if (!ignore) {
        setAuthor(data);
      }
    }
    loadAuthor();
    return () => { ignore = true; };
  }, []); // 空数组,只在页面挂载后获取一次!

  if (!author) return <p>加载作者信息中...</p>;
  return (
    <div style={{ background: '#eef2ff', padding: '10px', borderRadius: '6px', marginBottom: '15px' }}>
      <strong>{author.name}</strong> —— <span style={{ color: '#4f46e5' }}>{author.badge}</span>
    </div>
  );
}

// 组件 2:输入控制区,通过 props 传递输入状态与添加回调
function InputArea({ text, onTextChange, onAdd }) {
  return (
    <div style={{ marginBottom: '15px' }}>
      <input
        placeholder="写下你今天学到的 React 概念..."
        value={text}
        onChange={e => onTextChange(e.target.value)}
        style={{ padding: '8px', width: '260px', marginRight: '8px' }}
      />
      <button onClick={onAdd} style={{ padding: '8px 16px' }}>
        添加到复习清单
      </button>
    </div>
  );
}

// 组件 3:列表展示区,展示列表渲染与删除更新
function NoteList({ items, onDelete }) {
  if (items.length === 0) {
    return <p style={{ color: '#888' }}>复习清单为空,快添加一条吧!</p>;
  }

  return (
    <ul>
      {items.map(item => (
        <li key={item.id} style={{ marginBottom: '8px' }}>
          <span>{item.content}</span>
          <button
            onClick={() => onDelete(item.id)}
            style={{ marginLeft: '12px', color: '#ef4444' }}
          >
            删除
          </button>
        </li>
      ))}
    </ul>
  );
}

// 顶层主组件:协调状态与数据流动
export default function App() {
  const [inputText, setInputText] = useState('');
  const [notes, setNotes] = useState([
    { id: 1, content: '组件是大写开头的 JavaScript 纯函数' },
    { id: 2, content: '使用 Props 自顶向下单向传递数据' },
    { id: 3, content: '列表渲染必须指定稳定唯一的 key' }
  ]);

  // 点击添加:更新 State 数组
  function handleAdd() {
    if (!inputText.trim()) return;
    setNotes([
      ...notes,
      { id: Date.now(), content: inputText.trim() }
    ]);
    setInputText('');
  }

  // 点击删除:用 filter 不可变更新
  function handleDelete(id) {
    setNotes(notes.filter(n => n.id !== id));
  }

  return (
    <div style={{ maxWidth: '480px', margin: '40px auto', fontFamily: 'sans-serif' }}>
      <h1>我的 React 知识看板</h1>
      <AuthorCard />
      <InputArea
        text={inputText}
        onTextChange={setInputText}
        onAdd={handleAdd}
      />
      <NoteList
        items={notes}
        onDelete={handleDelete}
      />
    </div>
  );
}

结语

你已经完成了整个系统性 React 官方课程的学习! 你从零开始,走过了组件声明、JSX、Props、条件与列表渲染,跨越了事件响应、useState、渲染流水线与不可变对象数组更新,掌握了状态机、状态提升、Reducer 与 Context 的全套状态管理架构,并最终掌握了 Ref 与 Effect 这一走出 React 同步外部世界的强大脱围机制。

现在的你,已经完全有能力亲手独立搭建出架构健壮、性能优良、可读性极佳的现代 React 应用程序!

© 2026 React 官方教程