useEffect #

一、useEffect基础 #

1.1 什么是副作用 #

副作用是指函数执行过程中对外部世界的操作:

副作用类型 示例
数据获取 API 请求
订阅 事件监听、WebSocket
DOM操作 直接修改 DOM
定时器 setTimeout、setInterval
日志 console.log

1.2 基本语法 #

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

1.3 基本用法 #

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

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

  useEffect(() => {
    document.title = `点击了 ${count} 次`;
  }, [count]);

  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

二、依赖数组 #

2.1 三种依赖形式 #

javascript
function DependencyDemo() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  // 1. 无依赖数组:每次渲染后都执行
  useEffect(() => {
    console.log('每次渲染后执行');
  });

  // 2. 空数组:仅挂载时执行一次
  useEffect(() => {
    console.log('组件挂载时执行');
  }, []);

  // 3. 有依赖:依赖变化时执行
  useEffect(() => {
    console.log('count变化:', count);
  }, [count]);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <input value={name} onChange={(e) => setName(e.target.value)} />
    </div>
  );
}

2.2 依赖数组规则 #

javascript
// ✅ 正确:包含所有依赖
useEffect(() => {
  fetchData(userId);
}, [userId]);

// ❌ 错误:缺少依赖
useEffect(() => {
  fetchData(userId);
}, []); // userId变化时不会重新执行

// ✅ 使用函数式更新避免依赖
useEffect(() => {
  const timer = setInterval(() => {
    setCount(prev => prev + 1); // 不需要依赖count
  }, 1000);
  return () => clearInterval(timer);
}, []);

2.3 ESLint规则 #

javascript
// 推荐启用 ESLint 规则
// "react-hooks/exhaustive-deps": "warn"

// ESLint 会警告缺失的依赖
useEffect(() => {
  console.log(count);
}, []); // ⚠️ React Hook useEffect has a missing dependency: 'count'

三、清理函数 #

3.1 为什么需要清理 #

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

  useEffect(() => {
    const timer = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);
    
    // 清理函数:组件卸载时执行
    return () => {
      clearInterval(timer);
    };
  }, []);

  return <div>{seconds}秒</div>;
}

3.2 清理时机 #

javascript
useEffect(() => {
  console.log('副作用执行');
  
  return () => {
    console.log('清理函数执行');
  };
}, [count]);

// 执行顺序:
// 1. 组件挂载 → 副作用执行
// 2. count变化 → 清理函数执行 → 副作用执行
// 3. 组件卸载 → 清理函数执行

3.3 常见清理场景 #

javascript
function EventListener() {
  useEffect(() => {
    const handleResize = () => {
      console.log('窗口大小变化');
    };
    
    window.addEventListener('resize', handleResize);
    
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return <div>Window Resize Demo</div>;
}

function WebSocketDemo() {
  useEffect(() => {
    const ws = new WebSocket('ws://example.com');
    
    ws.onmessage = (event) => {
      console.log('收到消息:', event.data);
    };
    
    return () => {
      ws.close();
    };
  }, []);

  return <div>WebSocket Demo</div>;
}

四、常见使用场景 #

4.1 数据获取 #

javascript
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;

    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        
        if (!response.ok) {
          throw new Error('请求失败');
        }
        
        const data = await response.json();
        
        if (isMounted) {
          setUser(data);
          setError(null);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };

    fetchUser();

    return () => {
      isMounted = false;
    };
  }, [userId]);

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

4.2 事件监听 #

javascript
function MousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };

    window.addEventListener('mousemove', handleMouseMove);

    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []);

  return (
    <div>
      鼠标位置: {position.x}, {position.y}
    </div>
  );
}

4.3 定时器 #

javascript
function Countdown({ initialSeconds, onComplete }) {
  const [seconds, setSeconds] = useState(initialSeconds);

  useEffect(() => {
    if (seconds <= 0) {
      onComplete?.();
      return;
    }

    const timer = setTimeout(() => {
      setSeconds(prev => prev - 1);
    }, 1000);

    return () => {
      clearTimeout(timer);
    };
  }, [seconds, onComplete]);

  return <div>{seconds}秒</div>;
}

4.4 本地存储 #

javascript
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

function App() {
  const [name, setName] = useLocalStorage('name', '');

  return (
    <input
      value={name}
      onChange={(e) => setName(e.target.value)}
      placeholder="输入名字"
    />
  );
}

五、性能优化 #

5.1 避免不必要的执行 #

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

  useEffect(() => {
    // 防抖处理
    const timer = setTimeout(() => {
      search(query).then(setResults);
    }, 300);

    return () => clearTimeout(timer);
  }, [query]);

  return <Results data={results} />;
}

5.2 使用useMemo减少依赖 #

javascript
function Chart({ data, options }) {
  // 缓存计算结果
  const processedData = useMemo(() => {
    return data.map(item => transform(item));
  }, [data]);

  useEffect(() => {
    drawChart(processedData, options);
  }, [processedData, options]);

  return <canvas />;
}

5.3 分离关注点 #

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

  // 分离不同的副作用
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  useEffect(() => {
    fetchPosts(userId).then(setPosts);
  }, [userId]);

  return (
    <div>
      <UserCard user={user} />
      <PostList posts={posts} />
    </div>
  );
}

六、常见问题 #

6.1 无限循环 #

javascript
// ❌ 无限循环
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(count + 1); // 每次渲染都更新状态
  }, [count]);

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

// ✅ 添加条件判断
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    if (count < 10) {
      setCount(count + 1);
    }
  }, [count]);

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

6.2 闭包陷阱 #

javascript
// ❌ 闭包陷阱
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count); // 永远是初始值0
    }, 1000);

    return () => clearInterval(timer);
  }, []); // 空依赖导致闭包

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

// ✅ 使用ref解决
function Counter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);
  
  countRef.current = count;

  useEffect(() => {
    const timer = setInterval(() => {
      console.log(countRef.current); // 获取最新值
    }, 1000);

    return () => clearInterval(timer);
  }, []);

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

// ✅ 或者添加依赖
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count);
    }, 1000);

    return () => clearInterval(timer);
  }, [count]);

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

6.3 竞态条件 #

javascript
// ❌ 竞态条件
function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    fetchResults(query).then(setResults);
    // 如果query快速变化,可能显示旧结果
  }, [query]);

  return <Results data={results} />;
}

// ✅ 使用清理函数
function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    let isCurrent = true;

    fetchResults(query).then(data => {
      if (isCurrent) {
        setResults(data);
      }
    });

    return () => {
      isCurrent = false;
    };
  }, [query]);

  return <Results data={results} />;
}

// ✅ 使用AbortController
function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    const controller = new AbortController();

    fetchResults(query, { signal: controller.signal })
      .then(setResults)
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error(err);
        }
      });

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

  return <Results data={results} />;
}

七、useEffect模式 #

7.1 挂载时执行 #

javascript
useEffect(() => {
  console.log('组件挂载');
  
  return () => {
    console.log('组件卸载');
  };
}, []);

7.2 更新时执行 #

javascript
useEffect(() => {
  console.log('count更新:', count);
}, [count]);

7.3 挂载和更新都执行 #

javascript
useEffect(() => {
  console.log('组件渲染');
});

7.4 监听多个依赖 #

javascript
useEffect(() => {
  console.log('a或b变化:', a, b);
}, [a, b]);

八、最佳实践 #

8.1 依赖数组完整性 #

javascript
// ✅ 使用 ESLint 规则确保依赖完整
// eslint-disable-next-line react-hooks/exhaustive-deps

8.2 分离副作用 #

javascript
// ✅ 不同职责的副作用分离
useEffect(() => {
  // 数据获取
}, [userId]);

useEffect(() => {
  // 事件订阅
}, []);

useEffect(() => {
  // 文档标题
}, [title]);

8.3 自定义Hook封装 #

javascript
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;

    fetch(url)
      .then(res => res.json())
      .then(data => {
        if (isMounted) {
          setData(data);
          setLoading(false);
        }
      })
      .catch(err => {
        if (isMounted) {
          setError(err);
          setLoading(false);
        }
      });

    return () => {
      isMounted = false;
    };
  }, [url]);

  return { data, loading, error };
}

九、总结 #

要点 说明
依赖数组 正确设置所有依赖
清理函数 清理订阅、定时器等
竞态处理 使用标志位或AbortController
分离关注点 不同副作用分开处理

核心原则:

  • 确保依赖数组完整
  • 及时清理副作用
  • 处理好竞态条件
  • 避免闭包陷阱
最后更新:2026-03-26