useEffect 是 React 中,最难理解的 hook,在学习时,我们应该多花一点心思
在 React 中,useEffect 是用于处理副作用的 hook,它会在组件渲染后执行,并且会在组件卸载时执行
他的基础语法如下所示
引入 useEffect
1import { useEffect } from 'react';
在函数组件中使用 useEffect
01function App() {02useEffect(() => {03console.log('useEffect');04}, []);0506return (07<div>08<h1>Hello, World!</h1>09</div>10);11}
useEffect 接收两个参数
1useEffect(effect, deps);
在 React 中,由 state 的变化导致 UI 发生变化的过程是正常操作,其他操作行为:例如数据请求、直接手动修改 DOM 节点、直接操作页面,修改页面标题等、记录日志等都是副作用操作。
副作用操作是相对于操作 state 而言的
每一次因为 state 的改变,都有一次对应副作用函数的执行时机。如果 state 多次改变,那么就有多次对应副作用的执行时机
如下案例中,点击事件触发了 state 的改变,然后导致 UI render,从而导致副作用 effect 函数的执行
01import { useState, useEffect } from 'react';0203function Example() {04const [count, setCount] = useState(0);0506useEffect(() => {07// Update the document title using the browser API08document.title = `You clicked ${count} times`;09});1011return (12<div>13<p>You clicked {count} times</p>14<button onClick={() => setCount(count + 1)}>15Click me16</button>17</div>18);19}
在 React 18 中以及之前的版本中,我们通常都会使用 useEffect 来实现数据请求. 这是它最常用的场景,核心步骤是
setState 更新数据,JSX 会自动更新01import React, { useEffect, useState } from 'react'02import Skeleton from 'components/zmui/skeleton'03import Message from './message'04import { getMessage } from './api'0506export default function Demo04() {07const [content, update] = useState({ value: '' })08const [loading, setLoading] = useState(true)0910useEffect(() => {11getMessage().then(res => {12update(res)13setLoading(false)14})15}, []);1617return (18<div className='p-4'>19{loading ? <Skeleton type='header' /> : <Message message={content.value} />}20</div>21)22}23
此时,由于 useEffect 的依赖项为空数组,因此,effect 函数仅在组件初始化时执行一次
注意:在开发环境时,React 严格模式下,即使你传入的依赖项为空数组,也会执行两次
effect函数,他会主动将组件卸载并再次挂载。这是为了帮助你发现潜在的问题,以确保清理函数的正确。在生产环境时,不会执行两次
我们可以在依赖项数组中,放入 loading 状态,当我们想要更新请求时,只需要更新 loading 状态就可以让 effect 函数再次执行
当然,需要加一个判断,当 loading 状态为 true 时,才请求数据
01import { useEffect, useState } from 'react'02import Skeleton from 'components/zmui/skeleton'03import Message from './message'04import { getMessage } from './api'0506export default function Demo04() {07const [content, update] = useState({ value: '' })08const [loading, setLoading] = useState(true)0910useEffect(() => {11if (!loading) return;12getMessage().then(res => {13update(res)14setLoading(false)15})16}, [loading]);1718return (19<div className='p-4'>20<div className='text-right mb-4'>21<button className='button' onClick={() => setLoading(!loading)}>更新数据</button>22</div>23{loading ? <Skeleton type='header' /> : <Message message={content.value} />}24</div>25)26}27
有的时候,副作用函数 effect 执行会留下一些痕迹,因此 useEffect 提供了一种清除副作用的方式,我们称之为 cleanup effect
effect 与 cleanup effect 是一一对应的紧密关系。在语法表现上,我们定义一个回调函数由 effect 执行时返回,该函数就是 cleanup effect 函数
1useEffect(() => {2// dosomething34// 定义 cleanup effect 函数5return () => {6// clear something7}8}, [])
它的具体执行实际如下所示
通常,我们使用 cleanup 函数来取消定时器、取消订阅、取消网络请求等操作
由于 useEffect 的执行时机与 state 的变化密切相关,因此,我们可以利用 cleanup effect 来实现防抖效果,核心步骤如下
连续点击按钮,观察 2 个状态的变化
JSX 部分大多数内容是为了案例的演示样式更好看,所以代码比较繁杂,学习时,你可以仅关注逻辑部分,直接跳过他们
01import { useState, useEffect } from 'react';0203export default function debouncedCount() {04const [count, setCount] = useState(0);05const [debouncedCount, setDebouncedCount] = useState(0);06const [isWaiting, setIsWaiting] = useState(false);0708// 核心防抖逻辑09useEffect(() => {10// 只有当 count 变化时,进入 Effect11setIsWaiting(true);1213// 设定定时器14const timer = setTimeout(() => {15setDebouncedCount(count);16setIsWaiting(false);17}, 600); // 600ms 防抖时间1819// Cleanup 函数:如果在 600ms 内 count 再次变化,20// React 会先执行这个 cleanup 清除上一个定时器,21// 从而实现了“防抖”效果22return () => {23clearTimeout(timer);24};25}, [count]);2627const handleClick = () => {28setCount((c) => c + 1);29};3031return (32<div className="w-full p-8 bg-[#050505] text-neutral-200 flex flex-col items-center justify-center font-sans selection:bg-white selection:text-black">33<div className="relative z-10 w-full max-w-3xl px-6">34<div className="grid grid-cols-1 md:grid-cols-2 gap-px bg-neutral-800 border border-neutral-800">35{/* 左侧:实时响应 */}36<div className="bg-[#0a0a0a] p-10 flex flex-col justify-between h-64 relative group overflow-hidden">37<div className="flex justify-between items-start">38<span className="text-xs font-mono text-neutral-500 uppercase tracking-widest">State.立即更新</span>39<div className="w-2 h-2 bg-blue-500 animate-pulse"></div>40</div>4142<div className="font-mono text-7xl text-white font-light tracking-tighter">43{count.toString().padStart(2, '0')}44</div>4546<div className="text-xs text-neutral-400 font-mono">Updated: <span className="text-white">Instantly</span></div>47</div>4849{/* 右侧:防抖响应 */}50<div className="bg-[#0a0a0a] p-10 flex flex-col justify-between h-64 relative overflow-hidden">51<div className="flex justify-between items-start">52<span className="text-xs font-mono text-neutral-500 uppercase tracking-widest">State.防抖更新</span>53<div className={`w-2 h-2 transition-colors duration-200 ${isWaiting ? 'bg-amber-500' : 'bg-emerald-500'}`}></div>54</div>5556<div className={`font-mono text-7xl font-light tracking-tighter transition-colors duration-300 ${isWaiting ? 'text-neutral-600' : 'text-amber-500'}`}>57{debouncedCount.toString().padStart(2, '0')}58</div>5960<div className="text-xs text-neutral-400 font-mono flex items-center gap-2">61Status: {isWaiting ? <span className="text-amber-500 animate-pulse">WAITING (CLEANUP PENDING)</span> : <span className="text-emerald-500">SYNCED</span>}62</div>63</div>64</div>6566{/* 控制区 */}67<div className="mt-px grid grid-cols-1 xl:grid-cols-[1fr_auto] gap-px bg-neutral-800 border border-t-0 border-neutral-800">68{/* 说明文字 */}69<div className="bg-[#0a0a0a] p-6 flex items-center">70<div className="text-xs text-neutral-300 font-mono leading-relaxed">71<span className="text-white">Logic:</span> 当你在 600ms 内连续点击,<span className="text-amber-500">useEffect cleanup</span> 会清除上一次的定时器,阻止右侧数值更新。只有停止操作后,定时器才能走完。72</div>73</div>7475{/* 交互按钮 */}76<button onClick={handleClick} className="relative bg-white text-black px-12 py-6 hover:bg-amber-500 hover:text-black active:bg-amber-600 transition-colors duration-150 font-bold text-sm tracking-[0.2em] uppercase flex items-center justify-center gap-3 group">77<span>快速点击 / 连续点击</span>78</button>79</div>80</div>81</div>82);83}
通常情况下,我们不应该避免 useEffect 初始化时在开发环境中执行两次。因为这个机制存在的原因是为了帮助我们发现潜在的问题
但是在一些场景之下,你确实需要保持开发环境与生产环境的一致性,那么,我们可以通过取消严格模式来实现
在 react 中
01// 修改之前02ReactDOM.createRoot(document.getElementById('root')).render(03<React.StrictMode>04<App />05</React.StrictMode>06)0708// 修改之后09ReactDOM.createRoot(document.getElementById('root')).render(10<App />11)
在 next.js 中
1/** @type {import('next').NextConfig} */2const nextConfig = {3reactStrictMode: false, // 将其设置为 false4};56module.exports = nextConfig;
当然,你也可以通过创建一个 ref 标记,来避免内部逻辑中执行两次
01import { useEffect, useRef } from 'react';0203function MyComponent() {04const hasRun = useRef(false); // 创建一个标记0506useEffect(() => {07// 如果已经执行过,直接返回08if (hasRun.current) return;0910// 标记为已执行11hasRun.current = true;1213// --- 这里写你的业务逻辑 ---14console.log('这就只会执行一次了');15fetch('/api/data').then(...);1617}, []); // 空数组1819return <div>Hello World</div>;20}
如果只是入门级理解,那么阅读本文就行,如果要彻底透彻的理解,你需要结合闭包、事件循环的底层基础知识,你可以在下面两个专栏中深入学习
在后续的章节中,我们还需要学习更多关于 useEffect 的知识