深入理解 React useEffect

一、useEffect基础概念

1、什么是副作用(Side Effects)?
在React中,副作用是指那些与组件渲染结果无关的操作,例如:

  • 数据获取(API调用)
  • 手动修改DOM
  • 设置订阅或定时器
  • 记录日志

2、useEffect的基本语法

import { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // 副作用逻辑在这里执行
    
    return () => {
      // 清理函数(可选)
    };
  }, [dependency1, dependency2]); // 依赖数组(可选)
}

二、useEffect的三种适用方式

1、每次渲染后都执行

useEffect(() => {
  // 每次组件渲染后都会执行
  console.log('组件已渲染或更新');
});

2、仅在挂载时执行一次

useEffect(() => {
  // 只在组件挂载时执行一次
  console.log('组件已挂载');
  
  return () => {
    // 清理函数,在组件卸载时执行
    console.log('组件即将卸载');
  };
}, []); // 空依赖数组

3、依赖特定值变化时执行

useEffect(() => {
  // 当 count 或 name 变化时执行
  console.log(`Count: ${count}, Name: ${name}`);
  
  return () => {
    // 清理上一次的 effect
    console.log('清理上一次的 effect');
  };
}, [count, name]); // 依赖数组

三、useEffect执行机制详解

请添加图片描述

四、常见使用场景

1、数据获取

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    // 重置状态
    setLoading(true);
    setError(null);
    
    const fetchUser = async () => {
      try {
        const response = await fetch(`/api/users/${userId}`);
        const userData = await response.json();
        setUser(userData);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
    
    // 不需要清理函数,因为 fetch 会自动取消
  }, [userId]); // 当 userId 变化时重新获取

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

2、事件监听器

function WindowSizeTracker() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    // 添加事件监听
    window.addEventListener('resize', handleResize);
    
    // 清理函数:移除事件监听
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 空数组表示只在挂载/卸载时执行

  return (
    <div>
      窗口尺寸: {windowSize.width} x {windowSize.height}
    </div>
  );
}

3、定时器

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

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

    // 清理函数:清除定时器
    return () => {
      clearInterval(intervalId);
    };
  }, []); // 空依赖数组,只在挂载时设置定时器

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

4、手动操作DOM

function FocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    // 组件挂载后自动聚焦输入框
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []); // 空数组表示只在挂载时执行

  return <input ref={inputRef} placeholder="自动聚焦" />;
}

五、依赖数组的详细说明

1、依赖数组的规则

// ✅ 正确:包含所有依赖
useEffect(() => {
  document.title = `${title} - ${count} 次点击`;
}, [title, count]); // 所有依赖都声明

// ❌ 错误:缺少依赖
useEffect(() => {
  document.title = `${title} - ${count} 次点击`;
}, [title]); // 缺少 count 依赖

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

2、处理对象和函数依赖

function UserProfile({ user }) {
  // 使用 useMemo 记忆化对象
  const userStatus = useMemo(() => ({
    isActive: user.active,
    statusText: user.active ? '活跃' : '非活跃'
  }), [user.active]); // 只有当 user.active 变化时重新计算

  // 使用 useCallback 记忆化函数
  const updateUser = useCallback((updates) => {
    // 更新用户逻辑
  }, [user.id]); // 依赖 user.id

  useEffect(() => {
    // 使用记忆化的值和函数
    console.log(userStatus);
    updateUser({ lastLogin: new Date() });
  }, [userStatus, updateUser]); // 依赖记忆化的值

  return <div>用户状态: {userStatus.statusText}</div>;
}

六、useEffect的进阶用法

1、多个useEffect的使用

function ComplexComponent({ userId, autoRefresh }) {
  const [user, setUser] = useState(null);
  const [notifications, setNotifications] = useState([]);

  // 获取用户数据
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  // 获取通知(依赖用户数据)
  useEffect(() => {
    if (user) {
      fetchNotifications(user.id).then(setNotifications);
    }
  }, [user]); // 依赖 user

  // 自动刷新通知
  useEffect(() => {
    if (!autoRefresh || !user) return;
    
    const intervalId = setInterval(() => {
      fetchNotifications(user.id).then(setNotifications);
    }, 30000);

    return () => clearInterval(intervalId);
  }, [autoRefresh, user]); // 依赖 autoRefresh 和 user

  return (
    <div>
      {/* 渲染逻辑 */}
    </div>
  );
}

2、适用自定义Hook封装useEffect

// 自定义 Hook:使用防抖的搜索
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]); // 依赖 value 和 delay

  return debouncedValue;
}

// 在组件中使用
function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const debouncedQuery = useDebounce(query, 500);

  useEffect(() => {
    if (debouncedQuery) {
      searchAPI(debouncedQuery).then(setResults);
    } else {
      setResults([]);
    }
  }, [debouncedQuery]); // 依赖防抖后的查询

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="搜索..."
      />
      <ul>
        {results.map(result => (
          <li key={result.id}>{result.name}</li>
        ))}
      </ul>
    </div>
  );
}

七、常见问题与解决方案

1、无限循环问题

// ❌ 错误:导致无限循环
const [count, setCount] = useState(0);

useEffect(() => {
  setCount(count + 1); // 每次渲染都会更新 count,触发重新渲染
}, [count]); // 依赖 count

// ✅ 正确:使用函数式更新或无依赖
useEffect(() => {
  setCount(prevCount => prevCount + 1); // 不依赖外部 count 值
}, []); // 空依赖数组

2、异步操作处理

function AsyncComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    let isMounted = true; // 跟踪组件是否挂载
    
    const fetchData = async () => {
      try {
        const result = await fetch('/api/data');
        const jsonData = await result.json();
        
        if (isMounted) {
          setData(jsonData); // 只在组件仍挂载时更新状态
        }
      } catch (error) {
        if (isMounted) {
          console.error('获取数据失败:', error);
        }
      }
    };

    fetchData();

    return () => {
      isMounted = false; // 组件卸载时设置为 false
    };
  }, []);

  return <div>{data ? data.message : '加载中...'}</div>;
}

3、依赖函数的问题

function ProblematicComponent() {
  const [count, setCount] = useState(0);
  
  const logCount = () => {
    console.log('当前计数:', count);
  };

  // ❌ 问题:logCount 在每次渲染都是新函数
  useEffect(() => {
    logCount();
  }, [logCount]); // 导致每次渲染都执行

  // ✅ 解决方案1:将函数移到 useEffect 内部
  useEffect(() => {
    const logCount = () => {
      console.log('当前计数:', count);
    };
    
    logCount();
  }, [count]); // 只依赖 count

  // ✅ 解决方案2:使用 useCallback 记忆化函数
  const logCountMemoized = useCallback(() => {
    console.log('当前计数:', count);
  }, [count]); // 依赖 count

  useEffect(() => {
    logCountMemoized();
  }, [logCountMemoized]); // 依赖记忆化的函数

  return <button onClick={() => setCount(c => c + 1)}>增加</button>;
}

八、性能优化技巧

1、条件执行Effect

function ExpensiveComponent({ data, shouldProcess }) {
  useEffect(() => {
    if (shouldProcess) {
      // 只有 shouldProcess 为 true 时才执行昂贵操作
      performExpensiveOperation(data);
    }
  }, [data, shouldProcess]); // 仍然声明所有依赖
});

2、使用useMemo优化依赖

function OptimizedComponent({ items, filter }) {
  // 使用 useMemo 避免不必要的重新计算
  const filteredItems = useMemo(() => {
    return items.filter(item => item.includes(filter));
  }, [items, filter]); // 只有当 items 或 filter 变化时重新计算

  // effect 只依赖记忆化的值
  useEffect(() => {
    console.log('过滤后的项目:', filteredItems);
  }, [filteredItems]); // 依赖记忆化的数组

  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
}

3、避免不必要的Effect

// ❌ 不必要的 effect:可以在渲染期间直接计算
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

// ✅ 更好的方案:在渲染期间直接计算
const fullName = `${firstName} ${lastName}`;

九、最佳实践总结

1、明确依赖: 始终声明所有effect中使用的依赖项
2、适当清理: 对于订阅、定时器等,一定要提供清理函数
3、分离关注点: 使用多个useEffect分离不同的逻辑
4、避免无限循环: 谨慎设置状态,避免创建渲染循环
5、性能优化: 使用useMemo和useCallback优化依赖项
6、条件执行: 在effect内部添加条件判断,避免不必要的执行
7、异步处理: 正确处理异步操作的清理和竞态条件

总结

useEffect 是React函数组件的核心Hook,它使得副作用管理变得更加声明式和可预测。通过理解其执行机制、正确使用依赖数组、实现适当的清理逻辑,你可以编写出高效、可靠的React组件。

记住,useEffect 的核心思想是将副作用与渲染逻辑分离,让组件更专注于渲染UI,而将副作用操作放在统一的地方管理。这种分离使得代码更容易理解、测试和维护。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值