写好 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. 处理竞态条件 - 异步操作的安全性

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

相关推荐
哆啦A梦15885 小时前
搜索页面布局
前端·vue.js·node.js
_院长大人_6 小时前
el-table-column show-overflow-tooltip 只能显示纯文本,无法渲染 <p> 标签
前端·javascript·vue.js
SevgiliD6 小时前
el-table中控制单列内容多行超出省略及tooltip
javascript·vue.js·elementui
要加油哦~6 小时前
JS | 知识点总结 - 原型链
开发语言·javascript·原型模式
哆啦A梦15887 小时前
axios 的二次封装
前端·vue.js·node.js
阿珊和她的猫7 小时前
深入理解与手写发布订阅模式
开发语言·前端·javascript·vue.js·ecmascript·状态模式
yinuo7 小时前
一行 CSS 就能搞定!用 writing-mode 轻松实现文字竖排
前端
snow@li8 小时前
html5:拖放 / demo / 拖放事件(Drag Events)/ DataTransfer 对象方法
前端·html·拖放
爱看书的小沐8 小时前
【小沐杂货铺】基于Three.js渲染三维风力发电机(WebGL、vue、react、WindTurbine)
javascript·vue.js·webgl·three.js·opengl·风力发电机·windturbine