Bootstrap

react中hooks之useEffect 用法总结

1. 什么是函数的副作用(Side Effects)

副作用是指在组件渲染过程中,除了返回 JSX 之外的其他操作,例如:

  • 数据获取(API 调用)
  • 订阅数据源
  • 手动修改 DOM
  • 设置定时器
  • 存储数据
  • 日志记录
    纯函数是特定的输入只会有特定的输出,也就是说组件会输出特定的DOM给浏览器渲染,除去这份逻辑以外的操作就称之为副作用,比如获取数据,监听,订阅等等

2. useEffect 的执行时机

2.1 省略依赖项

useEffect(() => {
  console.log('每次渲染都会执行');
}); // 没有依赖项数组
  • 组件每次渲染都会执行
  • 包括首次渲染和后续更新

2.2 指定依赖项

useEffect(() => {
  console.log(`count 发生变化:${count}`);
}, [count]); // 依赖于 count
  • 首次渲染时执行
  • 依赖项发生变化时执行
  • 多个依赖项时,任意一个变化都会触发执行

2.3 空数组依赖项

useEffect(() => {
  console.log('只在组件挂载时执行一次');
}, []); // 空数组
  • 仅在组件首次渲染(挂载)时执行一次
  • 类似于 class 组件的 componentDidMount

3. 常见问题和最佳实践

3.1 避免依赖项循环

// ❌ 错误示例:造成无限循环
function BadExample() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    setCount(count + 1); // 直接修改依赖项
  }, [count]);
  
  return <div>{count}</div>;
}

// ✅ 正确示例:使用函数式更新
function GoodExample() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    setCount(prevCount => prevCount + 1);
  }, []); // 不需要依赖项
  
  return <div>{count}</div>;
}

3.2 分离关注点

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);

  // ✅ 分开声明不同功能的 useEffect
  useEffect(() => {
    // 获取用户信息
    fetchUser(userId).then(setUser);
  }, [userId]);

  useEffect(() => {
    // 获取用户帖子
    fetchUserPosts(userId).then(setPosts);
  }, [userId]);

  return (
    <div>
      <UserInfo user={user} />
      <UserPosts posts={posts} />
    </div>
  );
}

4. 清除副作用

4.1 清理函数的执行时机

清理函数会在以下情况执行:

  • 组件卸载时
  • 下一次 effect 执行前

4.2 事件监听示例

function WindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    
    // 添加事件监听
    window.addEventListener('resize', handleResize);
    
    // 清理函数
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 空依赖数组,只在挂载和卸载时执行

  return <div>Window width: {width}</div>;
}

4.3 定时器示例

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);

    // 清理函数:组件卸载时清除定时器
    return () => clearInterval(timer);
  }, []); // 空依赖数组

  return <div>Count: {count}</div>;
}

4.4 数据订阅示例

function DataSubscriber({ dataSource }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    let isSubscribed = true;

    const handleData = (newData) => {
      if (isSubscribed) {
        setData(newData);
      }
    };

    // 订阅数据源
    const subscription = dataSource.subscribe(handleData);

    // 清理函数:取消订阅
    return () => {
      isSubscribed = false;
      subscription.unsubscribe();
    };
  }, [dataSource]); // 依赖于 dataSource

  return <div>{data ? <DataView data={data} /> : 'Loading...'}</div>;
}

4.5 WebSocket 连接示例

function WebSocketComponent({ url }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const ws = new WebSocket(url);

    ws.onmessage = (event) => {
      setMessages(prev => [...prev, event.data]);
    };

    // 清理函数:关闭 WebSocket 连接
    return () => {
      ws.close();
    };
  }, [url]);

  return (
    <div>
      {messages.map((msg, index) => (
        <div key={index}>{msg}</div>
      ))}
    </div>
  );
}

5. 实际应用场景

5.1 表单自动保存

function AutoSaveForm() {
  const [content, setContent] = useState('');
  const [saving, setSaving] = useState(false);

  useEffect(() => {
    // 防抖处理
    const timeoutId = setTimeout(() => {
      if (content) {
        setSaving(true);
        saveContent(content)
          .then(() => setSaving(false));
      }
    }, 1000);

    return () => clearTimeout(timeoutId);
  }, [content]);

  return (
    <div>
      <textarea
        value={content}
        onChange={e => setContent(e.target.value)}
      />
      {saving && <span>Saving...</span>}
    </div>
  );
}

5.2 实时搜索

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  useEffect(() => {
    // 避免空查询
    if (!query.trim()) {
      setResults([]);
      return;
    }

    const abortController = new AbortController();

    async function fetchResults() {
      try {
        const response = await fetch(
          `/api/search?q=${query}`,
          { signal: abortController.signal }
        );
        const data = await response.json();
        setResults(data);
      } catch (error) {
        if (error.name === 'AbortError') {
          // 忽略中止的请求错误
          return;
        }
        console.error('搜索出错:', error);
      }
    }

    const timeoutId = setTimeout(fetchResults, 300);

    // 清理函数:取消请求和清除定时器
    return () => {
      clearTimeout(timeoutId);
      abortController.abort();
    };
  }, [query]);

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

6. 最佳实践总结

  1. 保持 effect 函数简洁,专注于单一功能
  2. 合理使用依赖项,避免不必要的执行
  3. 始终清理副作用,防止内存泄漏
  4. 使用条件语句控制 effect 的执行
  5. 考虑使用自定义 Hook 封装常用的副作用逻辑
  6. 在开发环境下使用 ESLint 的 exhaustive-deps 规则检查依赖项
  7. 使用 useCallback 和 useMemo 优化依赖项

通过合理使用 useEffect,我们可以优雅地处理组件的副作用,实现更复杂的交互逻辑,同时保持代码的可维护性和性能。

;