Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

hook:useEffect

  • 用来定义副作用,可以替代以前的生命周期

场景代码示例

  • 【场景 1】每次渲染都执行(不建议使用):

    • 每次组件更新后,都运行额外的代码

      // 场景:更新页面标题 - 每次渲染都执行
      useEffect(() => {
        document.title = `You clicked ${count} times`;
      }); //这里没有依赖项
      
    • 这样网页的标题就会随着每次渲染(无论是第一次 mount 还是后面的 update),都触发副作用,进而变更

    • ⚠️ 不建议使用原因: 任何状态变化都会触发,容易导致性能问题和不必要的执行

    • 如果要用以前的类去写,会很麻烦,需要 componentDidMount 和 componentDidUpdate

      componentDidMount() {
          document.title = `You clicked ${this.state.count} times`;
      }
      
      componentDidUpdate() {
          document.title = `You clicked ${this.state.count} times`;
      }
      
  • 【场景 2】依赖变化时执行(常用):

    • 数组内的依赖项变化时,才运行额外的代码,也属于副作用

      // 优化场景:更新页面标题 - 仅count值变化才执行
      useEffect(() => {
        document.title = `You clicked ${count} times`;
      }, [count]); //这里有依赖项
      
    • 只有 count 改变时才执行,避免了不必要的副作用

  • 【场景 3】仅首次渲染执行(常用):

    • 第一次 mount 时,请求 API 加载数据,也属于副作用

      // 数据请求 - 只在首次渲染时请求
      useEffect(() => {
        fetchData().then((data) => setData(data));
      }, []); //这里是空的依赖项
      
    • 相当于类组件的 componentDidMount,只在组件挂载时执行一次

  • 【场景 4】依赖变化时,先执行清理函数,再执行(常用):

    • 数组内的依赖项变化时,重新订阅,也属于副作用

      // friend.id 变化时 —— 先执行return后的函数,取消旧订阅,再建立新订阅
      useEffect(() => {
        ChatAPI.subscribe(friend.id, handleStatusChange);
        return () => ChatAPI.unsubscribe(friend.id, handleStatusChange);
      }, [friend.id]); //这里有依赖项
      
    • 执行流程: friend.id 变化 → 清理函数执行(取消旧订阅)→ effect 执行(建立新订阅)

  • 【场景 5】仅在组件卸载时执行清理:

    • 组件卸载之后,运行一些额外的代码,也属于副作用

      // 场景:监听事件 - 挂载时添加监听,卸载时移除
      useEffect(() => {
        window.addEventListener("resize", handleResize);
        return () => window.removeEventListener("resize", handleResize);
      }, []); //这里是空的依赖项
      
    • 这样子,会在卸载时,自动执行 return 后面的清理函数

    • 如果要用以前的类去写,会很麻烦,需要 componentDidMount 和 componentWillUnmount,像下面这样

      componentDidMount() {
          window.addEventListener("resize", handleResize);
      }
      
      componentWillUnmount() {
          window.removeEventListener("resize", handleResize);
      }
      

依赖数组的区别

场景依赖数组执行时机清理时机使用场景
场景 1每次渲染后-⚠️ 不建议
场景 2[dep]首次 或 依赖变化-常用(数据变化)
场景 3[]仅首次渲染-常用(数据请求)
场景 4[dep] + return首次 或 依赖变化依赖变化前 或 组件卸载时常用(订阅)
场景 5[] + return仅首次渲染仅组件卸载时常用(事件监听)

常见应用场景

场景是否需要清理依赖数组示例
数据请求[]fetch、axios
订阅/取消订阅[id]WebSocket、消息推送
事件监听/移除[]window.addEventListener
定时器设置/清除[]setTimeout、setInterval
手动 DOM 操作[dep]修改样式、聚焦元素
日志埋点[dep]页面访问统计
本地存储同步[data]localStorage.setItem

注意事项

  • ⚠️ 必须添加所有依赖项

    // ❌ 错误:使用了 count 但没有添加到依赖数组
    useEffect(() => {
      console.log(count); // count 始终是初始值
    }, []); // 缺少 count
    
    // ✅ 正确:添加所有依赖项
    useEffect(() => {
      console.log(count);
    }, [count]);
    
  • ⚠️ 清理函数的时机

    useEffect(() => {
      console.log("effect 执行");
      return () => {
        console.log("清理执行");
      };
    }, [dep]);
    
    // 执行顺序:
    // 1. 首次渲染:effect 执行
    // 2. dep 变化:清理执行 → effect 执行
    // 3. 组件卸载:清理执行
    
  • ⚠️ 异步函数不能直接作为 effect

    // ❌ 错误:useEffect 的回调不能是 async
    useEffect(async () => {
      const data = await fetchData();
      setData(data);
    }, []);
    
    // ✅ 正确:在内部定义 async 函数
    useEffect(() => {
      const loadData = async () => {
        const data = await fetchData();
        setData(data);
      };
      loadData();
    }, []);
    
    // ✅ 或者使用 then
    useEffect(() => {
      fetchData().then((data) => setData(data));
    }, []);
    
  • ⚠️ 避免在 effect 中直接修改状态导致无限循环

    // ❌ 错误:无限循环
    useEffect(() => {
      setCount(count + 1); // 触发渲染 → 再次执行 → 无限循环
    });
    
    // ✅ 正确:添加依赖数组或使用条件判断
    useEffect(() => {
      if (count < 10) {
        setCount(count + 1);
      }
    }, [count]);
    
  • ⚠️ effect 中使用的函数也需要加入依赖

    const fetchUser = () => {
      fetch(`/api/user/${userId}`).then(/* ... */);
    };
    
    // ❌ 错误:fetchUser 使用了 userId,但没有在依赖中
    useEffect(() => {
      fetchUser();
    }, []); // 缺少 fetchUser 或 userId
    
    // ✅ 方案 1:将函数移到 effect 内部
    useEffect(() => {
      const fetchUser = () => {
        fetch(`/api/user/${userId}`).then(/* ... */);
      };
      fetchUser();
    }, [userId]);
    
    // ✅ 方案 2:使用 useCallback 缓存函数
    const fetchUser = useCallback(() => {
      fetch(`/api/user/${userId}`).then(/* ... */);
    }, [userId]);
    
    useEffect(() => {
      fetchUser();
    }, [fetchUser]);
    
  • 使用 React DevTools 或 ESLint 检查依赖

    # 安装 ESLint 插件
    npm install eslint-plugin-react-hooks --save-dev
    
    // .eslintrc.json
    {
      "plugins": ["react-hooks"],
      "rules": {
        "react-hooks/rules-of-hooks": "error",
        "react-hooks/exhaustive-deps": "warn" // 检查依赖项
      }
    }
    
  • 清理函数返回值必须是函数或 undefined

    // ❌ 错误:返回值不是函数
    useEffect(() => {
      return "cleanup"; // 错误
    }, []);
    
    // ✅ 正确:返回函数
    useEffect(() => {
      return () => {
        console.log("cleanup");
      };
    }, []);
    
    // ✅ 或者不返回
    useEffect(() => {
      console.log("no cleanup needed");
    }, []);
    

实用技巧

  • 技巧 1:组合多个 effect,分离关注点

    // ✅ 推荐:将不相关的逻辑分开
    function Component() {
      // 数据请求
      useEffect(() => {
        fetchData();
      }, []);
    
      // 事件监听
      useEffect(() => {
        window.addEventListener("resize", handleResize);
        return () => window.removeEventListener("resize", handleResize);
      }, []);
    
      // 标题更新
      useEffect(() => {
        document.title = title;
      }, [title]);
    }
    
  • 技巧 2:使用自定义 Hook 封装复杂逻辑

    // 封装数据请求逻辑
    function useUserData(userId) {
      const [user, setUser] = useState(null);
      const [loading, setLoading] = useState(true);
    
      useEffect(() => {
        setLoading(true);
        fetch(`/api/user/${userId}`)
          .then((res) => res.json())
          .then((data) => {
            setUser(data);
            setLoading(false);
          });
      }, [userId]);
    
      return { user, loading };
    }
    
    // 使用
    function Component({ userId }) {
      const { user, loading } = useUserData(userId);
      if (loading) return <div>Loading...</div>;
      return <div>{user.name}</div>;
    }
    
  • 技巧 3:使用 AbortController 取消请求

    useEffect(() => {
      const controller = new AbortController();
    
      fetch("/api/data", { signal: controller.signal })
        .then((res) => res.json())
        .then((data) => setData(data))
        .catch((err) => {
          if (err.name === "AbortError") {
            console.log("Request cancelled");
          }
        });
    
      // 清理时取消请求
      return () => controller.abort();
    }, []);
    

与类组件生命周期的对应关系

类组件生命周期useEffect 等价写法说明
componentDidMountuseEffect(() => {}, [])挂载后执行一次
componentDidUpdateuseEffect(() => {}, [dep])依赖变化时执行
componentWillUnmountuseEffect(() => { return cleanup }, [])卸载前执行清理
组合使用useEffect(() => {})每次渲染后执行

注意: useEffect 的思维方式与生命周期不同,应该从「同步副作用」的角度思考,而非「生命周期方法」