写好 React useEffect 的终极指南

🎯 核心心法:转变思维模式

从生命周期到副作用同步

  • 旧思维:"在组件挂载时执行这个副作用"
  • 新思维 :"当这些依赖项变化时,我需要同步外部系统与组件状态"

useEffect 的本质:在渲染后,根据依赖项将外部系统与当前 props/state 同步


⚡ 五大核心原则

1. 详尽的依赖项数组

规则:所有在 effect 内部使用的外部值都必须声明为依赖

js 复制代码
// ❌ 依赖缺失 - 闭包陷阱
function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);

  useEffect(() => {
    fetch(`/api/products/${productId}`).then(setProduct);
  }, []); // 缺少 productId

  return <div>{product?.name}</div>;
}

// ✅ 依赖完整
function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);

  useEffect(() => {
    fetch(`/api/products/${productId}`).then(setProduct);
  }, [productId]); // 所有依赖都声明

  return <div>{product?.name}</div>;
}

启用 ESLint 规则:这是你最好的朋友!

2. 单一职责原则

每个 useEffect 只负责一件事

js 复制代码
// ❌ 混合职责
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useTheme();

  useEffect(() => {
    fetchUser(userId).then(setUser);     // 职责1:数据获取
    document.title = `Profile`;          // 职责2:DOM操作  
    document.body.className = theme;     // 职责3:样式应用
  }, [userId, theme]);

  // ...
}

// ✅ 职责分离
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useTheme();

  // Effect 1:只负责数据获取
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  // Effect 2:只负责文档标题
  useEffect(() => {
    if (user) {
      document.title = `Profile of ${user.name}`;
    }
  }, [user]);

  // Effect 3:只负责主题应用
  useEffect(() => {
    document.body.className = theme;
  }, [theme]);

  // ...
}

3. 提供清理函数

清理时机:组件卸载时 + 下次 effect 执行前

js 复制代码
function EventListenerComponent() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

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

    // 1. 添加事件监听
    window.addEventListener('mousemove', handleMouseMove);

    // ✅ 返回清理函数
    return () => {
      // 2. 移除事件监听
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []); // 空依赖:只在挂载时执行

  return <div>Position: {position.x}, {position.y}</div>;
}

需要清理的资源

  • 事件监听器
  • 定时器
  • WebSocket 连接
  • 订阅
  • 异步操作

4. 减少不必要的依赖

在依赖完整的前提下优化

技巧 1:函数式更新 State
js 复制代码
// ❌ 依赖 count
useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1); // 需要 count 依赖
  }, 1000);
  return () => clearInterval(id);
}, [count]);

// ✅ 无依赖
useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1); // 函数式更新
  }, 1000);
  return () => clearInterval(id);
}, []);
技巧 2:函数移入 Effect 内部
js 复制代码
// ❌ 外部函数需要作为依赖
const fetchData = () => { /* ... */ };
useEffect(() => {
  fetchData();
}, [fetchData]);

// ✅ 函数在内部,无需依赖
useEffect(() => {
  const fetchData = () => { /* ... */ };
  fetchData();
}, []);
技巧 3:使用 useCallback/useMemo 稳定引用
js 复制代码
// ✅ 稳定函数引用
const fetchProduct = useCallback(() => {
  // ...
}, [productId]); // 只有 productId 变化时重新创建

useEffect(() => {
  fetchProduct();
}, [fetchProduct]); // 现在依赖是稳定的

5. 处理竞态条件

防止过时数据覆盖最新数据

js 复制代码
function DataFetcher({ id }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    let ignore = false; // 忽略标志
    
    const fetchData = async () => {
      const result = await fetch(`/api/data/${id}`);
      const jsonData = await result.json();
      
      if (!ignore) { // 只有不忽略时才更新状态
        setData(jsonData);
      }
    };

    fetchData();

    return () => {
      ignore = true; // 清理时设置忽略标志
    };
  }, [id]); // id 变化时取消上一次请求

  return <div>{data?.name}</div>;
}

🛠️ 常用代码模板

模板 1:数据获取(完整版)

js 复制代码
useEffect(() => {
  let ignore = false;
  
  const fetchData = async () => {
    try {
      setLoading(true);
      setError(null);
      
      const response = await fetch(`/api/data/${id}`);
      if (!response.ok) throw new Error('Network error');
      
      const result = await response.json();
      
      if (!ignore) {
        setData(result);
      }
    } catch (err) {
      if (!ignore) {
        setError(err.message);
      }
    } finally {
      if (!ignore) {
        setLoading(false);
      }
    }
  };

  fetchData();

  return () => {
    ignore = true;
  };
}, [id]);

模板 2:事件监听器

js 复制代码
useEffect(() => {
  const handleKeyPress = (event) => {
    if (event.key === 'Escape') {
      onClose();
    }
  };

  document.addEventListener('keydown', handleKeyPress);

  return () => {
    document.removeEventListener('keydown', handleKeyPress);
  };
}, [onClose]); // onClose 需要用 useCallback 包裹

模板 3:定时器

js 复制代码
useEffect(() => {
  const intervalId = setInterval(() => {
    setCount(prev => prev + 1); // 函数式更新,无依赖
  }, 1000);

  return () => clearInterval(intervalId);
}, []); // 空依赖:定时器只创建一次

模板 4:第三方库集成

js 复制代码
useEffect(() => {
  const chart = new ChartJS(ctx, {
    type: 'line',
    data: chartData,
    options: chartOptions
  });

  return () => {
    chart.destroy(); // 清理第三方库实例
  };
}, [chartData, chartOptions]); // 数据变化时重新创建

模板 5:表单验证

js 复制代码
useEffect(() => {
  const errors = validateForm(formState);
  setFormErrors(errors);
}, [formState]); // formState 变化时重新验证

🔧 高级模式

自定义 Hook 封装

js 复制代码
// 自定义数据获取 Hook
function useApiData(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

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

    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url);
        const result = await response.json();
        
        if (!ignore) {
          setData(result);
        }
      } catch (err) {
        if (!ignore) {
          setError(err.message);
        }
      } finally {
        if (!ignore) {
          setLoading(false);
        }
      }
    };

    fetchData();

    return () => { ignore = true; };
  }, [url]);

  return { data, loading, error };
}

// 使用
function UserProfile({ userId }) {
  const { data: user, loading, error } = useApiData(`/api/users/${userId}`);
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <div>{user.name}</div>;
}

依赖项调试

js 复制代码
// 调试依赖项变化
useEffect(() => {
  console.log('Effect running with:', { prop1, state1 });
  // 副作用逻辑...
}, [prop1, state1]);

// 或者使用 useWhatChanged
function useWhatChanged(dependencies, names) {
  const prevRef = useRef(dependencies);
  
  useEffect(() => {
    dependencies.forEach((dep, i) => {
      if (dep !== prevRef.current[i]) {
        console.log(`${names[i]} changed:`, {
          from: prevRef.current[i],
          to: dep
        });
      }
    });
    prevRef.current = dependencies;
  });
}

// 使用
useWhatChanged([prop1, state1], ['prop1', 'state1']);

✅ 检查清单

在编写每个 useEffect 时,问自己:

1. 目的明确

  • 这个 Effect 在同步什么外部系统?
  • 它解决了什么问题?

2. 依赖完整

  • 所有使用的外部值都声明为依赖了吗?
  • ESLint 规则通过了吗?

3. 清理必要

  • 需要返回清理函数吗?
  • 清理了所有创建的资源吗?

4. 职责单一

  • 这个 Effect 只做一件事吗?
  • 需要拆分成多个 Effect 吗?

5. 依赖优化

  • 可以使用函数式更新减少依赖吗?
  • 可以使用 useCallback/useMemo 稳定引用吗?

6. 竞态安全

  • 异步操作有竞态条件风险吗?
  • 使用了忽略标志吗?

7. 性能考虑

  • 依赖项变化会导致不必要的执行吗?
  • 可以使用条件执行优化吗?

🎯 总结

写好 useEffect 的关键是掌握 "同步思维" 而非 "生命周期思维"

  1. 明确同步目标 - 知道你在同步什么
  2. 完整声明依赖 - 信任但不盲从 ESLint
  3. 及时清理资源 - 防止内存泄漏
  4. 保持单一职责 - 一个 Effect 做一件事
  5. 优化依赖关系 - 在完整的基础上精简
  6. 处理竞态条件 - 异步操作的安全性

遵循这些原则,你就能写出可靠、可维护、高性能的副作用代码!

相关推荐
Web极客码21 分钟前
深入了解WordPress网站访客意图
服务器·前端·wordpress
幺风1 小时前
Claude Code 源码分析 — Tool/MCP/Skill 可扩展工具系统
前端·javascript·ai编程
vjmap1 小时前
唯杰地图CAD图层加高性能特效扩展包发布
前端·gis
ZC跨境爬虫1 小时前
3D 地球卫星轨道可视化平台开发 Day7(AI异步加速+卫星系列精简+AI Agent自动评论)
前端·人工智能·3d·html·json
ID_180079054731 小时前
淘宝 API 上货 / 商品搬家 业务场景实现 + JSON 返回示例
前端·javascript·json
M ? A1 小时前
Vue 动态组件在 React 中,VuReact 会如何实现?
前端·javascript·vue.js·经验分享·react.js·面试·vureact
vipbic2 小时前
独立开发复盘:我用 Uni-app + Strapi v5 肝了一个“会上瘾”的打卡小程序
前端·微信小程序
IT_陈寒3 小时前
Vite的热更新突然失效,原来是因为这个配置
前端·人工智能·后端
ZC跨境爬虫3 小时前
3D 地球卫星轨道可视化平台开发 Day8(分步渲染200颗卫星+ 前端分页控制)
前端·python·3d·重构·html
竹林8183 小时前
RainbowKit快速集成多链钱包连接,我如何从“连不上”到“丝滑切换”
前端·javascript