react

useEffect 深度解析

By AI-Writer 10 min read

前言

useEffect 是 React 中处理副作用的核心 Hook。副作用包括数据获取、订阅、手动修改 DOM 等在组件渲染过程中产生的”额外操作”。理解 useEffect 的执行时机和依赖机制,是写出健壮 React 应用的关键。

useEffect 基础

什么是副作用

**副作用(Side Effect)**是指那些影响组件外部世界的操作:

jsx
import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  // 这是一个副作用:数据获取
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setUser(data));
  }, [userId]); // 依赖 userId

  if (!user) return <div>加载中...</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  );
}

基本语法

jsx
useEffect(() => {
  // 副作用逻辑
  return () => {
    // 清理函数(可选)
  };
}, [依赖数组]);

执行时机

渲染之后执行

useEffect 在组件渲染并应用到 DOM 之后执行:

jsx
function Demo() {
  useEffect(() => {
    // 这个代码在组件渲染到 DOM 后执行
    console.log('组件已挂载到 DOM');
  });

  return <div>Hello</div>;
}

执行时机对比

plaintext
组件函数执行

JSX 生成并渲染到 DOM

useEffect 回调执行  ← 副作用在这里运行
jsx
function TimingDemo() {
  const [count, setCount] = useState(0);

  console.log('1. 渲染中...');  // 最先执行

  useEffect(() => {
    console.log('2. useEffect 执行');  // DOM 更新后
  });

  console.log('3. 渲染完成');  // 紧跟渲染

  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}

依赖数组

依赖数组控制 useEffect 何时重新执行,是最核心也最容易出错的部分。

不指定依赖(每次渲染都执行)

jsx
useEffect(() => {
  // 没有依赖数组 = 每次渲染后都执行
  document.title = `计数:${count}`;
});

空数组(只执行一次)

jsx
useEffect(() => {
  // 只在首次渲染时执行,类似 componentDidMount
  const subscription = api.subscribe(handleChange);

  return () => {
    // 组件卸载时清理,类似 componentWillUnmount
    subscription.unsubscribe();
  };
}, []);  // 空数组 = 只执行一次

指定依赖(按需执行)

jsx
function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    // 仅当 query 变化时执行
    const controller = new AbortController();

    fetch(`/api/search?q=${query}`, { signal: controller.signal })
      .then(res => res.json())
      .then(data => setResults(data))
      .catch(err => {
        if (err.name !== 'AbortError') throw err;
      });

    return () => controller.abort();
  }, [query]);  // query 变化时重新执行

  return <ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>;
}

清理函数

清理函数在以下情况执行:

  1. 组件卸载时
  2. useEffect 重新执行前(在下一次 effect 执行前)

清理订阅

jsx
function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();

    // 清理函数:断开连接
    return () => {
      connection.disconnect();
      console.log(`已断开房间 ${roomId}`);
    };
  }, [roomId]);

  return <h1>房间 {roomId}</h1>;
}

清理定时器

jsx
function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    // 清理函数:清除定时器
    return () => clearInterval(intervalId);
  }, []);  // 只需创建一次定时器

  return <div>已运行 {seconds} 秒</div>;
}

清理副作用请求

jsx
function DataFetcher({ url }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    let cancelled = false;

    fetch(url)
      .then(res => res.json())
      .then(json => {
        // 检查是否已被取消
        if (!cancelled) {
          setData(json);
        }
      });

    // 清理:忽略过期请求的结果
    return () => {
      cancelled = true;
    };
  }, [url]);

  return <div>{data ? JSON.stringify(data) : '加载中...'}</div>;
}

常见误用场景

误用 1:依赖数组遗漏

jsx
// ❌ 错误:count 在 effect 中使用但未加入依赖
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 这个 effect 依赖 count,但未在依赖数组中声明
    document.title = `计数:${count}`;
  }, []);  // 应该是 [count]

  // count 变化时,title 不会更新
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

// ✓ 正确
useEffect(() => {
  document.title = `计数:${count}`;
}, [count]);  // 声明依赖

误用 2:过度依赖

jsx
// ❌ 错误:创建不必要的依赖
function Component({ user }) {
  const [name, setName] = useState(user.name);

  useEffect(() => {
    setName(user.name);
    // user 对象每次渲染都是新引用!
    // 即使 name 相同,也会触发更新
  }, [user]);  // user 每次渲染都变化

  return <input value={name} onChange={e => setName(e.target.value)} />;
}

// ✓ 正确:只依赖实际需要的数据
useEffect(() => {
  setName(user.name);
}, [user.name]);  // 只依赖具体的值

误用 3:在 Effect 中直接修改 state

jsx
// ❌ 错误:可能导致无限循环
function WrongPattern({ data }) {
  const [processed, setProcessed] = useState([]);

  useEffect(() => {
    const result = processData(data);
    setProcessed(result);  // 这会触发重新渲染
  });  // 没有依赖数组,每次渲染都执行

  return <List items={processed} />;
}

// ✓ 正确:使用 useMemo 替代(派生值不需要 Effect)
function CorrectPattern({ data }) {
  const processed = useMemo(() => processData(data), [data]);
  return <List items={processed} />;
}

误用 4:将 props 直接作为初始 state

jsx
// ❌ 错误:props 变化时不会同步更新
function Counter({ initialCount }) {
  const [count, setCount] = useState(initialCount);  // 只用第一次的值

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <button onClick={() => setCount(initialCount)}>重置</button>
    </div>
  );
}

// ✓ 正确:如果需要同步 props,使用 key 强制重置
<Counter key={userId} initialCount={0} />

// 或使用 Effect 同步
function SyncedCounter({ initialCount }) {
  const [count, setCount] = useState(initialCount);

  useEffect(() => {
    setCount(initialCount);
  }, [initialCount]);

  return <div>{count}</div>;
}

替代方案

派生值用 useMemo

jsx
// ❌ 不要用 Effect 计算派生值
function BadExample({ items, filter }) {
  const [filteredItems, setFilteredItems] = useState(items);

  useEffect(() => {
    setFilteredItems(items.filter(item => item.includes(filter)));
  }, [items, filter]);

  return <List items={filteredItems} />;
}

// ✓ 用 useMemo
function GoodExample({ items, filter }) {
  const filteredItems = useMemo(
    () => items.filter(item => item.includes(filter)),
    [items, filter]
  );

  return <List items={filteredItems} />;
}

重置特定 state 用 key

jsx
// 当用户切换时,希望表单重置
function LoginForm({ userId }) {
  return <Form key={userId} />;  // key 变化时,组件完全重新创建
}

事件逻辑用事件处理函数

jsx
function Counter() {
  const [count, setCount] = useState(0);

  function handleReset() {
    setCount(0);  // 同步操作不需要 Effect
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <button onClick={handleReset}>重置</button>
    </div>
  );
}

完整示例:实时搜索

jsx
import { useState, useEffect } from 'react';

function Search({ api }) {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    // 空搜索词时不请求
    if (!query.trim()) {
      setResults([]);
      return;
    }

    const controller = new AbortController();
    setLoading(true);

    const timeoutId = setTimeout(async () => {
      try {
        const data = await api.search(query, controller.signal);
        setResults(data);
      } catch (err) {
        if (err.name !== 'AbortError') {
          console.error('搜索失败', err);
        }
      } finally {
        setLoading(false);
      }
    }, 300);  // 防抖

    return () => {
      clearTimeout(timeoutId);
      controller.abort();
    };
  }, [query, api]);

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="搜索..."
      />
      {loading && <span>搜索中...</span>}
      <ul>
        {results.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}

ESLint 规则

React 的 exhaustive-deps 规则(来自 eslint-plugin-react-hooks)可以帮助检测依赖数组问题:

json
{
  "rules": {
    "react-hooks/exhaustive-deps": "warn"
  }
}

启用后,ESLint 会警告:

  • effect 中使用的变量未在依赖数组中声明
  • 依赖数组中有不必要的依赖

小结

  • 执行时机:useEffect 在渲染并应用到 DOM 后执行,不阻塞渲染
  • 依赖数组
    • 无数组:每次渲染后都执行
    • 空数组:只在首次渲染时执行
    • 有数组:依赖变化时执行
  • 清理函数:在组件卸载或 effect 重新执行前运行,用于清理订阅、定时器、中止请求
  • 常见错误:遗漏依赖、过度依赖、将派生值放入 state
  • 替代方案:派生值用 useMemo,事件逻辑用处理函数,重置用 key

掌握了 useEffect 后,下一篇文章我们将学习 useRef,了解如何在 React 中直接操作 DOM。

#react #hooks #useEffect #进阶

评论

A

Written by

AI-Writer

Related Articles

react
#10

表单处理

掌握 React 中的表单处理模式,包括受控与非受控组件、表单验证,以及 React Hook Form 的使用

Read More
react
#1

React 元素与 JSX 语法

深入理解 JSX 的本质——JavaScript 的语法扩展,掌握虚拟 DOM 概念、JSX 编译原理以及开发中常见的错误与最佳实践

Read More
react
#2

组件与 Props

掌握 React 函数组件的定义与使用,理解 Props 的传递机制、children 属性、默认值设置,以及良好的组件设计原则

Read More