React18+快速入门 - 2.核心hooks之useEffect

一、useEffect 基础概念

1.1 什么是 useEffect

useEffect 是 React Hooks 中用于处理副作用的 Hook。副作用包括:数据获取、订阅、手动修改 DOM、设置定时器等。

1.2 基本语法

javascript 复制代码
useEffect(() => {
  // 副作用代码
  return () => {
    // 清理函数(可选)
  };
}, [dependencies]); // 依赖数组

二、useEffect 的 5 种使用模式

2.1 每次渲染都执行(不推荐)

javascript 复制代码
useEffect(() => {
  console.log('每次组件渲染都会执行');
});
// ⚠️ 注意:没有第二个参数

2.2 仅挂载时执行一次

javascript 复制代码
useEffect(() => {
  console.log('只在组件挂载时执行一次');
  
  // 模拟组件挂载时的操作
  const timer = setTimeout(() => {
    console.log('定时器执行');
  }, 1000);
  
  // 清理函数
  return () => {
    console.log('组件卸载时清理');
    clearTimeout(timer);
  };
}, []); // ⭐ 空依赖数组

2.3 依赖特定状态执行

javascript 复制代码
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    console.log(`userId 变化为: ${userId}`);
    
    // 根据 userId 获取用户数据
    fetchUserData(userId).then(data => {
      setUser(data);
    });
  }, [userId]); // ⭐ 依赖 userId,当 userId 变化时执行
  
  return <div>{user?.name}</div>;
}

2.4 多个依赖项

javascript 复制代码
function ChatRoom({ roomId, userId }) {
  useEffect(() => {
    console.log(`连接到聊天室 ${roomId},用户 ${userId}`);
    
    const connection = createConnection(roomId, userId);
    connection.connect();
    
    return () => {
      console.log('断开连接');
      connection.disconnect();
    };
  }, [roomId, userId]); // ⭐ 多个依赖项
}

2.5 清理函数(Cleanup)

javascript 复制代码
useEffect(() => {
  // 1. 订阅事件
  const handleResize = () => {
    console.log('窗口大小变化');
  };
  
  window.addEventListener('resize', handleResize);
  
  // 2. 设置定时器
  const intervalId = setInterval(() => {
    console.log('定时执行');
  }, 1000);
  
  // 3. 清理函数(组件卸载或依赖变化时执行)
  return () => {
    console.log('执行清理');
    window.removeEventListener('resize', handleResize);
    clearInterval(intervalId);
  };
}, []);

三、实际应用场景

3.1 数据获取

javascript 复制代码
function DataFetcher() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    let isMounted = true; // 防止组件卸载后设置状态
    
    const fetchData = async () => {
      setLoading(true);
      setError(null);
      
      try {
        const response = await fetch('https://api.example.com/data');
        const result = await response.json();
        
        if (isMounted) {
          setData(result);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };
    
    fetchData();
    
    return () => {
      isMounted = false;
    };
  }, []); // 空依赖数组:只获取一次
  
  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;
  
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

3.2 监听浏览器事件

javascript 复制代码
function WindowTracker() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });
  
  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };
    
    // 添加事件监听
    window.addEventListener('resize', handleResize);
    
    // 立即执行一次获取初始值
    handleResize();
    
    // 清理函数
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 空数组:只在挂载/卸载时执行
  
  return (
    <div>
      窗口尺寸: {windowSize.width} x {windowSize.height}
    </div>
  );
}

3.3 表单输入防抖(Debouncing)

javascript 复制代码
function SearchInput() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  
  useEffect(() => {
    if (!query.trim()) {
      setResults([]);
      return;
    }
    
    // 设置防抖定时器
    const timer = setTimeout(() => {
      console.log(`搜索: ${query}`);
      
      // 模拟 API 调用
      fetchSearchResults(query).then(data => {
        setResults(data);
      });
    }, 500); // 500ms 延迟
    
    // 清理函数:在 query 变化时清除上一个定时器
    return () => {
      clearTimeout(timer);
    };
  }, [query]); // query 变化时触发
  
  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>
  );
}

3.4 与 localStorage 交互

javascript 复制代码
function ThemeToggle() {
  const [theme, setTheme] = useState(() => {
    // 从 localStorage 读取初始值
    const saved = localStorage.getItem('theme');
    return saved || 'light';
  });
  
  // 1. 保存 theme 到 localStorage
  useEffect(() => {
    localStorage.setItem('theme', theme);
    console.log(`主题已保存: ${theme}`);
  }, [theme]); // theme 变化时保存
  
  // 2. 应用主题到 body
  useEffect(() => {
    document.body.className = theme;
    console.log(`主题已应用: ${theme}`);
  }, [theme]);
  
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };
  
  return (
    <button onClick={toggleTheme}>
      切换到 {theme === 'light' ? '深色' : '浅色'} 主题
    </button>
  );
}

3.5 控制台日志(开发调试)

javascript 复制代码
function DebugComponent({ value, user }) {
  // 记录 value 的变化
  useEffect(() => {
    console.log('value 变化:', value);
  }, [value]);
  
  // 记录 user 对象的变化(深度比较问题)
  useEffect(() => {
    console.log('user 对象变化:', user);
  }, [user]); // ⚠️ 注意:如果 user 是对象,每次都会触发
  
  // 更好的做法:依赖特定属性
  useEffect(() => {
    console.log('user.id 变化:', user.id);
  }, [user.id]); // 只依赖 id
  
  return <div>调试组件</div>;
}

四、高级用法和最佳实践

4.1 多个 useEffect 的使用

javascript 复制代码
function ComplexComponent({ id, type }) {
  // 分开不同的副作用,提高可读性
  useEffect(() => {
    // 1. 数据获取
    fetchData(id);
  }, [id]);
  
  useEffect(() => {
    // 2. 事件监听
    setupEventListeners(type);
    
    return () => {
      cleanupEventListeners();
    };
  }, [type]);
  
  useEffect(() => {
    // 3. 文档标题
    document.title = `组件 ${id}`;
  }, [id]);
  
  return <div>复杂组件</div>;
}

4.2 避免无限循环

javascript 复制代码
function InfiniteLoopExample() {
  const [count, setCount] = useState(0);
  
  // ❌ 错误:无限循环!
  // useEffect(() => {
  //   setCount(count + 1);
  // }, [count]); // count 变化会触发 effect,effect 又会改变 count
  
  // ✅ 正确:使用函数式更新
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(prevCount => prevCount + 1); // 函数式更新
    }, 1000);
    
    return () => clearInterval(timer);
  }, []); // 空依赖,只在挂载时设置定时器
  
  return <div>计数: {count}</div>;
}

4.3 自定义 Hook 封装

javascript 复制代码
// 自定义 Hook:useLocalStorage
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });
  
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(storedValue));
    } catch (error) {
      console.error(error);
    }
  }, [key, storedValue]);
  
  return [storedValue, setStoredValue];
}

// 使用自定义 Hook
function UserSettings() {
  const [settings, setSettings] = useLocalStorage('user-settings', {
    theme: 'light',
    language: 'zh-CN',
  });
  
  const updateTheme = (theme) => {
    setSettings(prev => ({ ...prev, theme }));
  };
  
  return (
    <div>
      当前主题: {settings.theme}
      <button onClick={() => updateTheme('dark')}>切换主题</button>
    </div>
  );
}

4.4 useEffect 中的异步函数

javascript 复制代码
function AsyncEffectExample() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    // 方法1:定义异步函数并立即调用
    const fetchData = async () => {
      try {
        const response = await fetch('/api/data');
        const result = await response.json();
        setData(result);
      } catch (error) {
        console.error('获取数据失败:', error);
      }
    };
    
    fetchData();
    
    // 方法2:使用 IIFE(立即调用函数表达式)
    /*
    (async () => {
      const response = await fetch('/api/data');
      const result = await response.json();
      setData(result);
    })();
    */
    
    // ⚠️ 注意:不能直接使用 async function 作为 effect 函数
    // useEffect(async () => { ... }, []); // ❌ 错误!
    
  }, []);
  
  return <div>{data ? JSON.stringify(data) : '加载中...'}</div>;
}

五、常见问题和解决方案

5.1 依赖项管理

javascript 复制代码
function DependencyExample() {
  const [count, setCount] = useState(0);
  const [multiplier, setMultiplier] = useState(1);
  
  // ✅ 正确:包含所有依赖
  useEffect(() => {
    console.log(`计算结果: ${count * multiplier}`);
  }, [count, multiplier]); // 包含所有使用的值
  
  // ❌ 错误:遗漏依赖(ESLint 会警告)
  // useEffect(() => {
  //   console.log(`计算结果: ${count * multiplier}`);
  // }, [count]); // 缺少 multiplier
  
  // 特殊情况:如果确实不需要依赖变化时触发
  const stableCallback = useCallback(() => {
    console.log(`稳定回调,count: ${count}`);
  }, [count]); // useCallback 缓存函数
  
  useEffect(() => {
    // 使用稳定回调
    stableCallback();
  }, [stableCallback]); // 依赖稳定的回调
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>增加计数</button>
      <button onClick={() => setMultiplier(m => m * 2)}>双倍乘数</button>
    </div>
  );
}

5.2 条件执行

javascript 复制代码
function ConditionalEffect({ shouldFetch, id }) {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    // 条件执行
    if (!shouldFetch || !id) {
      return; // 提前返回,不执行副作用
    }
    
    let isMounted = true;
    
    const fetchData = async () => {
      try {
        const response = await fetch(`/api/data/${id}`);
        const result = await response.json();
        
        if (isMounted) {
          setData(result);
        }
      } catch (error) {
        console.error(error);
      }
    };
    
    fetchData();
    
    return () => {
      isMounted = false;
    };
  }, [shouldFetch, id]); // 依赖条件
  
  return <div>{data ? data.name : '未获取数据'}</div>;
}

六、性能优化

6.1 避免不必要的执行

javascript 复制代码
function OptimizedComponent({ items, filter }) {
  // 计算衍生数据(使用 useMemo)
  const filteredItems = useMemo(() => {
    console.log('重新计算 filteredItems');
    return items.filter(item => item.includes(filter));
  }, [items, filter]); // 依赖 items 和 filter
  
  // 只有 filteredItems 变化时才执行
  useEffect(() => {
    console.log('过滤结果变化,执行副作用');
    // 处理 filteredItems
  }, [filteredItems]); // 依赖计算后的值
  
  return (
    <ul>
      {filteredItems.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
}

6.2 使用 useRef 存储不触发重新渲染的值

javascript 复制代码
function RefInEffect() {
  const [count, setCount] = useState(0);
  const previousCountRef = useRef();
  const intervalRef = useRef();
  
  useEffect(() => {
    // 存储上一次的值
    previousCountRef.current = count;
  });
  
  useEffect(() => {
    // 使用 ref 存储定时器 ID
    intervalRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    
    return () => {
      clearInterval(intervalRef.current);
    };
  }, []);
  
  const previousCount = previousCountRef.current;
  
  return (
    <div>
      当前计数: {count}
      {previousCount !== undefined && (
        <div>上一次计数: {previousCount}</div>
      )}
    </div>
  );
}

七、useEffect 与 useLayoutEffect 的区别

javascript 复制代码
function EffectComparison() {
  const [value, setValue] = useState(0);
  
  // useEffect:在浏览器绘制后异步执行
  useEffect(() => {
    console.log('useEffect 执行');
    
    if (value === 0) {
      // 可能会看到闪烁
      setValue(100);
    }
  }, [value]);
  
  // useLayoutEffect:在浏览器绘制前同步执行
  useLayoutEffect(() => {
    console.log('useLayoutEffect 执行');
    
    // 适合操作 DOM,避免闪烁
    // document.body.style.backgroundColor = 'red';
  }, [value]);
  
  return (
    <button onClick={() => setValue(0)}>
      重置为 0
    </button>
  );
}

总结要点

  1. 依赖数组决定执行时机

    • []:只在挂载和卸载时执行
    • [dep1, dep2]:依赖变化时执行
    • 无数组:每次渲染都执行
  2. 清理函数

    • 返回一个函数用于清理副作用
    • 在组件卸载或依赖变化前执行
  3. 最佳实践

    • 分离不同的副作用到多个 useEffect
    • 包含所有必要的依赖
    • 使用自定义 Hook 封装复杂逻辑
    • 注意无限循环问题
  4. 常见用途

    • 数据获取
    • 事件监听
    • 订阅/取消订阅
    • 操作 DOM
    • 设置/清除定时器

记住:useEffect 是处理副作用的工具,不是处理同步状态逻辑的地方。对于复杂的状态逻辑,考虑使用 useReducer 或其他状态管理方案。

相关推荐
zlpzlpzyd3 小时前
vue.js 3中全局组件和局部组件的区别
前端·javascript·vue.js
浩星3 小时前
css实现类似element官网的磨砂屏幕效果
前端·javascript·css
一只小风华~3 小时前
Vue.js 核心知识点全面解析
前端·javascript·vue.js
2022.11.7始学前端3 小时前
n8n第七节 只提醒重要的待办
前端·javascript·ui·n8n
SakuraOnTheWay3 小时前
React Grab实践 | 记一次与Cursor的有趣对话
前端·cursor
阿星AI工作室4 小时前
gemini3手势互动圣诞树保姆级教程来了!附提示词
前端·人工智能
徐小夕4 小时前
知识库创业复盘:从闭源到开源,这3个教训价值百万
前端·javascript·github
xhxxx4 小时前
函数执行完就销毁?那闭包里的变量凭什么活下来!—— 深入 JS 内存模型
前端·javascript·ecmascript 6
StarkCoder4 小时前
求求你试试 DiffableDataSource!别再手算 indexPath 了(否则迟早崩)
前端
fxshy4 小时前
Cursor 前端Global Cursor Rules
前端·cursor