从零开始学前端 | 第三十一章:useEffect 与数据请求

📅 2026/7/22 11:54:01
从零开始学前端 | 第三十一章:useEffect 与数据请求
本章定位上一章我们已经把 React 页面里非常核心的三条主线串起来了一组数据怎么渲染成一组界面元素。某块内容要不要显示怎么根据条件决定。多个组件之间怎么围绕数据流协作。也就是说到现在为止你已经不只是会写单个组件而是已经开始能组织一个稍微像样的 React 页面了。例如你已经能理解这些场景用map渲染列表根据状态显示空状态或正常内容通过父子组件配合切换分类这一步非常重要。但如果你继续往下写很快会遇到一个新的问题有些逻辑并不是“根据已有数据直接渲染页面”这么简单。例如页面刚加载时要去请求文章列表某个状态变化后要同步更新页面标题组件启动后要开启一个定时器组件离开页面时要把定时器清掉组件挂载后要监听窗口大小变化这些事情有一个共同点它们不只是“根据当前数据返回 JSX”而是在和组件外部世界打交道。这类逻辑在 React 里通常被叫做副作用而useEffect就是 React 里处理这类问题的核心基础工具之一。所以你可以把这一章理解成从“页面内部的数据和渲染”走到“组件和外部世界的交互”。本章学习目标学完这一章后你应该能做到理解什么是副作用。知道哪些场景通常属于副作用。理解为什么副作用不应该直接写在组件渲染过程中。知道useEffect是做什么的。掌握useEffect的最基础写法。理解依赖数组的三种常见用法。学会用useEffect在组件加载后执行逻辑。理解为什么数据请求是最典型的副作用之一。学会给数据请求补上加载中、失败和成功三种状态。理解什么是清理副作用。学会在定时器等场景中做基础清理。理解状态和副作用各自负责什么。避开这一阶段最常见的几个坑。为下一章继续整理 React 表单、状态与项目结构打下基础。一、为什么学到这里需要useEffect前面几章我们一直在建立一条很重要的 React 主线数据变界面跟着变。这条主线非常关键而且依然成立。但真实页面里还有另一类事情也很常见组件显示出来以后还需要额外做一点事。例如页面第一次出现后请求接口数据。用户切换分类后重新请求当前分类下的内容。数量变化后同步更新浏览器标签标题。组件开始显示后启动一个定时器。组件消失前把定时器或监听器清掉。这些事情都不是“直接在 JSX 里写出来”就结束的。它们更像是渲染完成之后还要再执行的一些逻辑。而useEffect正是 React 用来处理这类逻辑的入口之一。二、什么是副作用先给一个当前阶段最够用的理解副作用就是那些发生在“正常渲染页面之外”并且会和外部环境发生交互的逻辑。这个定义你现在不用一下子背得很学术。你只要先抓住一句话如果某段逻辑不只是“根据数据返回界面”而是在额外做事它就很可能和副作用有关。1. 哪些事情经常算副作用例如请求接口数据操作浏览器标题document.title启动和清理定时器绑定和移除事件监听读写本地存储2. 哪些事情通常不算副作用例如根据数组map出一组 JSX根据状态决定显示“加载中”还是列表根据props拼出标题文字这些更像是纯粹根据现有数据计算界面结果。这类逻辑通常应该留在渲染主线里而不是塞进副作用。三、为什么副作用不能直接写在组件渲染过程中这一点非常关键。很多人第一次学 React 副作用时最自然的想法是那我直接把请求代码写在组件函数里不就行了吗看起来好像挺顺但往往会有问题。先看一个“看起来像能工作”的写法function ArticleList() { fetch(/api/articles) .then(function (response) { return response.json(); }) .then(function (data) { console.log(data); }); return p文章列表/p; }1. 这段代码最大的问题是什么不是fetch语法错了而是组件函数本身应该主要负责“根据当前数据返回界面”而不是一渲染就直接执行外部副作用。如果组件多渲染几次这段请求逻辑也可能跟着重复执行。2. 当前阶段最值得建立的意识你可以先这样理解渲染逻辑负责描述界面长什么样副作用逻辑负责在合适时机“额外做事”而useEffect就是 React 给你的一种明确表达方式这段逻辑不是渲染本身而是渲染之后要做的副作用。四、什么是useEffect先给一个当前阶段最够用的理解useEffect是 React 提供的一个工具用来在组件渲染后执行副作用逻辑。这句话非常重要。你可以先把它拆成两层来理解它是 React 提供的工具。它用来处理副作用。1. 当前阶段最实用的理解方式你可以把useEffect理解成一句很接地气的话页面显示出来之后我还要做点事就常常会想到useEffect。2. 它和前面学过的useState是什么关系你可以先这样区分useState存状态useEffect处理副作用也就是说一个更像“存数据”一个更像“做额外动作”。五、useEffect的最基础写法先看最基础的结构import { useEffect } from react; function Demo() { useEffect(function () { console.log(组件显示后执行这段逻辑); }); return puseEffect 示例/p; }1. 这段代码想表达什么它想表达的是组件渲染后执行useEffect里的这段逻辑。2. 当前阶段你最需要先抓住什么先不要急着深挖底层。你先记住这件事就够了useEffect里放的通常不是直接渲染 JSX 的代码而是渲染之外要做的事情。六、依赖数组怎么理解这一节非常重要。因为很多初学者一看到useEffect最容易懵的就是最后那个数组。你现在先不用死背所有细节但一定要先理解三种最常见的情况。七、第一种情况不写依赖数组例如useEffect(function () { console.log(每次渲染后都会执行); });当前阶段你可以先理解成组件每次渲染完成后这段逻辑都会执行。这类写法不是不能用但初学阶段要比较谨慎。因为如果里面放了比较重的逻辑可能就会执行得比你想象中频繁。八、第二种情况依赖数组是空数组[]例如useEffect(function () { console.log(组件初次显示后执行一次); }, []);当前阶段你可以先把它理解成组件第一次显示到页面后执行一次。这也是非常常见的一种写法。尤其是在这些场景里页面初次加载后请求数据组件出现后启动一个定时器组件出现后做一次初始化逻辑九、第三种情况依赖数组里写具体依赖例如useEffect(function () { console.log(当 keyword 变化后执行); }, [keyword]);当前阶段你可以先理解成组件第一次显示后会执行以后每当keyword变化时也会重新执行。这类写法非常适合某个搜索词变化后重新请求数据某个分类变化后更新结果某个状态变化后同步浏览器标题1. 当前阶段怎么记最稳你可以先记这张“口语版小抄”不写数组每次渲染后都执行空数组[]第一次显示后执行写具体依赖第一次显示后执行依赖变化时再执行这已经足够支撑你进入本章的大多数场景了。十、一个和状态联动的最小useEffect例子先看一个不涉及请求的简单例子。import { useEffect, useState } from react; function CounterPage() { const [count, setCount] useState(0); useEffect(function () { document.title 当前数量${count}; }, [count]); return ( section p当前数量{count}/p button onClick{function () { setCount(count 1); }} 加一 /button /section ); }1. 这个例子最值得你观察什么它在说明一件很关键的事当count变化后不只是页面里的数字会变副作用逻辑也可以跟着执行。这里的副作用就是同步更新浏览器标签标题。2. 为什么这个例子很适合入门因为它把三件事连了起来状态count事件触发setCountuseEffect根据状态变化做额外动作这几乎就是 React 很多交互页面的基础骨架。十一、为什么数据请求是最典型的副作用之一现在我们进入本章最核心的场景数据请求。为什么请求数据一定会经常和useEffect一起出现因为请求数据本质上不是“直接渲染 JSX”的过程而是组件显示出来以后再去外部拿数据。例如打开文章列表页后请求文章数据切换分类后重新请求对应分类内容输入搜索词后请求新的搜索结果这些都非常符合副作用的特点发生在渲染之外会和外部接口或环境交互完成后会再回来更新状态所以你可以先记住一句话React 里的数据请求通常是“副作用负责去拿数据状态负责把数据存下来界面再根据状态显示结果”。十二、组件加载后请求数据时通常要准备哪些状态这是非常实用的一节。很多初学者第一次写请求只盯着“请求成功后拿到什么数据”但真实页面通常不止一种状态。最常见的至少有三类加载中请求失败请求成功1. 为什么要专门有“加载中”因为请求不是瞬间完成的。在等待结果时界面通常也要给用户反馈。例如“文章加载中…”按钮禁用骨架屏2. 为什么要有“失败状态”因为请求并不总能成功。可能出现网络异常接口报错服务不可用这时候如果没有失败状态用户通常只会看到页面像卡住了一样。3. 所以一个最基础的请求页面常常会有这些状态例如articleListisLoadingerrorMessage这套组合在真实项目里非常常见。十三、一个最基础的数据请求示例下面我们用一个“文章列表”示意场景来看看最基础的写法。先说明一下这里的/api/articles只是示意接口地址用来帮助你理解流程。1. 先看状态准备import { useEffect, useState } from react; function ArticleListPage() { const [articleList, setArticleList] useState([]); const [isLoading, setIsLoading] useState(true); const [errorMessage, setErrorMessage] useState(); return p文章列表页/p; }2. 再看请求逻辑放进useEffectuseEffect(function () { async function loadArticles() { try { setIsLoading(true); setErrorMessage(); const response await fetch(/api/articles); if (!response.ok) { throw new Error(文章数据加载失败); } const data await response.json(); setArticleList(data); } catch (error) { setErrorMessage(文章数据加载失败请稍后再试); } finally { setIsLoading(false); } } // 组件初次显示后请求文章数据 loadArticles(); }, []);3. 最后根据不同状态决定显示什么if (isLoading) { return p文章加载中.../p; } if (errorMessage) { return p{errorMessage}/p; } return ( ul {articleList.map(function (article) { return li key{article.id}{article.title}/li; })} /ul );4. 这个例子真正串起了什么它把这一章最核心的流程串起来了组件第一次显示useEffect开始请求数据请求过程中显示加载中请求失败时显示错误提示请求成功后把数据放进状态界面根据最新状态显示列表这就是 React 里最基础、也最经典的数据请求工作流。十四、为什么这里要把请求逻辑写成函数再调用很多人看到刚才的例子会注意到这一段async function loadArticles() { // ... } loadArticles();这不是为了显得正式而是因为在useEffect里初学阶段更稳的写法通常是“先定义一个异步函数再在内部调用它”。1. 当前阶段为什么推荐这样理解因为这样写通常更清楚你能一眼看出“请求逻辑是一整块”你能把try / catch / finally放得更自然整个useEffect的结构会更容易读2. 当前阶段最重要的不是背写法而是看清职责你要先看清楚的是useEffect负责决定“什么时候请求”而内部函数负责表达“怎么请求”。这条分工意识非常有价值。十五、当依赖变化时useEffect怎么重新请求数据前面我们讲过依赖数组里写具体依赖时依赖变化后useEffect会重新执行。这对数据请求特别有用。例如当前分类切换后重新请求对应分类的数据。先看一个很简单的结构useEffect(function () { async function loadArticlesByCategory() { const response await fetch(/api/articles?category${currentCategory}); const data await response.json(); setArticleList(data); } loadArticlesByCategory(); }, [currentCategory]);1. 这段代码在说明什么它在说明初次显示时会请求一次以后每当currentCategory变化时也会重新请求。2. 这在真实项目里为什么非常常见因为很多页面本来就会围绕“某个条件变化 - 重新取数据”来工作。例如切换分类切换页码切换排序方式更新搜索词所以你要开始慢慢建立一个反应某个条件一变就要重新做副作用这时就会想到“依赖数组里是不是该放它”。十六、什么是清理副作用现在我们进入本章另一个非常重要的部分清理。先给一个当前阶段最够用的理解清理副作用就是在组件离开页面前或者下一次副作用重新执行前把之前留下的外部影响处理掉。这个定义你现在先不用追得太细。你先抓住最常见的感觉就够了我启动了一个东西就要考虑以后要不要把它收掉。例如开了定时器要不要清掉绑了事件监听要不要移除建立了某些订阅要不要取消这些都属于清理意识。十七、useEffect里怎么写清理逻辑在useEffect里清理逻辑通常通过return一个函数来表达。先看最基础的结构useEffect(function () { console.log(启动某些副作用); return function () { console.log(清理副作用); }; }, []);1. 当前阶段怎么理解这个返回函数你可以先把它理解成React 在适当时机会调用这个函数用来做收尾工作。2. 当前阶段最实用的记法你先记住这一句就够了useEffect里如果需要清理就在里面return一个清理函数。十八、一个定时器清理例子这个例子非常经典也很适合入门。import { useEffect, useState } from react; function TimerCounter() { const [count, setCount] useState(0); useEffect(function () { const timerId window.setInterval(function () { setCount(function (prevCount) { return prevCount 1; }); }, 1000); return function () { // 组件离开前把定时器清掉 clearInterval(timerId); }; }, []); return p计时秒数{count}/p; }1. 这个例子最值得你看懂什么它说明了一条很重要的流程组件显示后启动定时器定时器不断更新状态状态变化带动界面变化组件离开前清理定时器2. 为什么清理这一步不能省如果你开了定时器却不清理后面就可能出现组件都不在了但外部逻辑还在继续跑。这会让代码越来越难控。所以从这一章开始你要慢慢建立一个很重要的习惯只要副作用创建了额外资源就顺手想一想它以后怎么收尾十九、再认识一个常见清理场景事件监听除了定时器事件监听也是很典型的清理场景。例如useEffect(function () { function handleResize() { console.log(window.innerWidth); } window.addEventListener(resize, handleResize); return function () { window.removeEventListener(resize, handleResize); }; }, []);1. 这个例子想说明什么它说明只要你在useEffect里往外部环境“挂了点东西”就要考虑在合适时机把它解开。2. 当前阶段先建立什么意识就够了不是每种副作用你现在都要写得很熟。但你至少要先建立这个反应副作用不只有“执行”还有“清理”。二十、状态和副作用各自负责什么这一节非常重要。因为很多初学者学到useEffect后容易开始把所有逻辑都往里面塞。这通常不是一个好方向。你要慢慢建立一个更清楚的分工意识1. 状态负责“存当前结果”例如articleListisLoadingerrorMessagekeyword这些都是“当前页面需要知道的数据”。2. 副作用负责“做额外动作”例如请求接口更新标题启动定时器绑定监听器3. 渲染负责“根据当前状态显示界面”例如加载中就显示加载提示报错就显示错误信息有数据就显示列表4. 当前阶段最值得记住的分工你可以先把它总结成状态存结果副作用拿数据或做外部动作渲染根据状态决定界面。这句话非常关键。二十一、不要把所有逻辑都塞进一个useEffect这是本章最想提前提醒你的一个习惯问题。很多人一学会useEffect就会开始这样想既然很多事情都能放进去那我就把所有逻辑都放里面。这通常会带来两个问题代码越来越长越来越难读状态逻辑、副作用逻辑、纯计算逻辑混在一起1. 更稳的做法是什么你可以先建立这个判断如果只是根据现有状态计算结果先考虑直接算不急着上useEffect如果真的是和外部环境交互再考虑useEffect如果副作用分成两块职责就尽量拆开写2. 一个很实用的自问方式每次想写useEffect时先问自己一句这段逻辑是不是“渲染之外必须做的额外动作”如果不是就别急着塞进去。二十二、初学者最容易踩的几个坑这一节建议你认真看。因为useEffect本身不算最难的 API但它很容易因为概念没分清而用乱。1. 坑一把请求直接写在组件函数里这几乎是最常见的问题之一。你一旦把外部请求逻辑直接写进组件主体很容易让渲染和副作用纠缠在一起。2. 坑二看见逻辑就想塞进useEffect不是所有逻辑都需要useEffect。如果某个结果能直接根据当前props和state算出来通常先不要急着放进副作用。3. 坑三没搞清依赖数组的意图初学阶段最容易出现的不是“完全不会写”而是明明想表达“初次加载执行一次”结果写成了别的效果。所以你要反复回到这三个基本判断不写数组空数组[]写具体依赖4. 坑四有副作用启动却没有清理意识尤其是定时器事件监听这类副作用一定要慢慢建立“启动后要考虑清理”的习惯。5. 坑五把状态、副作用、渲染职责混成一团更稳的理解应该是状态存数据副作用做额外动作渲染负责显示界面只要这三层开始分开你的 React 代码就会清楚很多。二十三、本章实践练习这一章的练习重点是把“组件加载后做事、根据结果更新状态、再让界面显示不同结果”这条主线真正练熟。1. 练习 1用useEffect同步页面标题请你写一个计数器组件要求页面上显示当前数量点击按钮后数量增加浏览器标题跟着显示最新数量这个练习的重点是先把“状态变化 -useEffect做额外动作”这条链路走顺。2. 练习 2实现一个最基础的文章请求页面请你完成一个文章列表示意页面要求至少包含文章列表状态加载中状态失败状态组件加载后请求数据这个练习会帮你真正把useEffectfetchstate条件渲染串起来。3. 练习 3让分类变化后重新请求数据请你给页面增加一个分类状态例如allreacttypescript要求当前分类变化时useEffect重新执行根据分类重新请求或重新加载对应数据这个练习会帮助你真正理解依赖数组为什么会和数据请求强相关。4. 练习 4写一个带清理逻辑的定时器组件请你完成一个简单计时器要求组件显示后开始计时每秒更新一次数字组件卸载前清理定时器这个练习的重点是建立副作用不仅要执行还要清理的意识。二十四、学习重点提示这一章请你重点记住下面这些话副作用是那些发生在正常渲染之外、会和外部环境交互的逻辑。useEffect的核心作用是在组件渲染后处理副作用。数据请求、更新标题、定时器、事件监听都是典型副作用场景。依赖数组不一样useEffect的执行时机就不一样。页面请求数据时通常要同时考虑成功、失败、加载中三种状态。状态负责存结果副作用负责做外部动作渲染负责显示界面。只要副作用创建了额外资源就要考虑如何清理。不要把所有逻辑都塞进一个useEffect先分清是不是副作用。如果某段逻辑只是根据现有数据直接计算结果通常先不要急着用useEffect。如果你只记一句话请记住useEffect的核心不是“多学一个新 API”而是学会把“渲染页面”和“渲染后还要做的事”分开处理。二十五、本章小结这一章我们正式把 React 从“页面内部的数据流和组件协作”推进到了“组件和外部世界的交互”。你已经理解了什么是副作用为什么副作用不应该直接写在组件渲染过程中useEffect的最基础作用和写法依赖数组的三种常见情况为什么数据请求是最典型的副作用之一请求页面为什么常常要有加载中、失败、成功三种状态什么是清理副作用为什么定时器和事件监听常常要配合清理逻辑状态、副作用和渲染各自负责什么更重要的是你开始建立一种非常关键的 React 认知组件不只是根据数据显示界面还经常需要在合适的时机去请求数据、同步外部环境并做好收尾工作。这一步非常关键。因为从这里开始你已经不只是会写“页面上的交互”而是在开始真正进入更接近真实项目的 React 开发流程。二十六、课后思考题请你认真思考下面这些问题什么样的逻辑通常可以被理解为副作用为什么说请求数据不应该直接写在组件渲染主体里不写依赖数组、写空数组、写具体依赖这三种情况最基础的区别是什么为什么说数据请求往往不只是“拿到数据”还要考虑加载中和失败状态useEffect里的清理函数在解决什么问题为什么说状态、副作用和渲染职责最好分开理解为什么“不是所有逻辑都该放进useEffect”建议你把这些问题用自己的话写下来。只要你能把这些问题讲清楚说明你已经真正开始进入 React 副作用与异步数据处理的主线了。二十七、下一篇预告下一章我们会进入从零开始学前端 | 第三十二章React 表单、状态整理与项目结构到那时你会继续解决这些很实际的问题表单页面怎样组织得更清楚多个状态放在一起时怎样整理得不乱小项目的组件、页面和文件应该怎么分层也就是说下一章开始我们会从“副作用与异步请求”继续走到React 小项目结构整理与状态组织的基础。