我在项目中这样处理useEffect依赖引用类型,同事直呼内行

大家好,我是小杨。作为一名有6年经验的前端开发,今天想和大家聊聊我在使用useEffect时遇到的一个经典坑点------引用类型依赖问题。

记得刚开始用React Hooks时,我写过这样的代码:

jsx 复制代码
function UserProfile({ userId }) {
  const [userData, setUserData] = useState(null);
  const fetchConfig = {
    method: 'GET',
    headers: { 'Authorization': 'Bearer token' }
  };

  useEffect(() => {
    const fetchUser = async () => {
      const response = await fetch(`/api/users/${userId}`, fetchConfig);
      const data = await response.json();
      setUserData(data);
    };
    
    fetchUser();
  }, [userId, fetchConfig]); // 这里埋了个坑!

  return <div>{/* 用户信息展示 */}</div>;
}

看起来没问题?但实际上,这个useEffect会在每次渲染时都执行!为什么呢?

为什么引用类型依赖会出问题?

在JavaScript中,对象、数组、函数都是引用类型。每次组件重新渲染时,即使内容没变,它们都会被重新创建,内存地址不同。

jsx 复制代码
function Example() {
  const obj = { name: 'John' };
  const arr = [1, 2, 3];
  const func = () => {};
  
  useEffect(() => {
    console.log('Effect triggered');
  }, [obj, arr, func]); // 每次都会触发!
  
  return <div>Test</div>;
}

我是如何解决这个问题的?

方案一:将依赖移到useEffect内部

jsx 复制代码
function UserProfile({ userId }) {
  const [userData, setUserData] = useState(null);

  useEffect(() => {
    // 将配置移到effect内部
    const fetchConfig = {
      method: 'GET',
      headers: { 'Authorization': 'Bearer token' }
    };
    
    const fetchUser = async () => {
      const response = await fetch(`/api/users/${userId}`, fetchConfig);
      const data = await response.json();
      setUserData(data);
    };
    
    fetchUser();
  }, [userId]); // 现在只需要依赖userId

  return <div>{/* 用户信息展示 */}</div>;
}

方案二:使用useMemo缓存引用类型

jsx 复制代码
function DataTable({ data, sortOptions }) {
  const [sortedData, setSortedData] = useState([]);
  
  // 使用useMemo缓存排序配置
  const memoizedSortOptions = useMemo(() => ({
    key: sortOptions.key,
    direction: sortOptions.direction
  }), [sortOptions.key, sortOptions.direction]);

  useEffect(() => {
    const result = sortData(data, memoizedSortOptions);
    setSortedData(result);
  }, [data, memoizedSortOptions]); // 现在依赖稳定了

  return <Table data={sortedData} />;
}

方案三:使用useCallback缓存函数

jsx 复制代码
function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  
  // 使用useCallback缓存函数
  const createConnection = useCallback(() => {
    return {
      connect: () => console.log(`连接到房间 ${roomId}`),
      disconnect: () => console.log(`断开连接`)
    };
  }, [roomId]);

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    
    return () => connection.disconnect();
  }, [createConnection]); // 依赖稳定的函数引用

  return <div>{/* 聊天界面 */}</div>;
}

我在实际项目中的经验总结

场景一:API请求配置

错误写法:

jsx 复制代码
function ProductsPage() {
  const [products, setProducts] = useState([]);
  const queryParams = { category: 'electronics', page: 1 };

  useEffect(() => {
    fetchProducts(queryParams).then(setProducts);
  }, [queryParams]); // 依赖问题!

  return <ProductList products={products} />;
}

正确写法:

jsx 复制代码
function ProductsPage() {
  const [products, setProducts] = useState([]);
  const [category, setCategory] = useState('electronics');
  const [page, setPage] = useState(1);

  useEffect(() => {
    const queryParams = { category, page };
    fetchProducts(queryParams).then(setProducts);
  }, [category, page]); // 依赖基本类型,安全!

  return <ProductList products={products} />;
}

场景二:事件处理函数

错误写法:

jsx 复制代码
function SearchBox() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  
  const handleSearch = (searchQuery) => {
    searchAPI(searchQuery).then(setResults);
  };

  useEffect(() => {
    handleSearch(query);
  }, [query, handleSearch]); // handleSearch每次都会变!

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

正确写法:

jsx 复制代码
function SearchBox() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  
  const handleSearch = useCallback((searchQuery) => {
    searchAPI(searchQuery).then(setResults);
  }, []);

  useEffect(() => {
    handleSearch(query);
  }, [query, handleSearch]); // 现在handleSearch是稳定的

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

高级技巧:自定义Hook封装

在实际项目中,我经常这样封装:

jsx 复制代码
function useDeepCompareEffect(callback, dependencies) {
  const ref = useRef();
  
  if (!deepEqual(dependencies, ref.current)) {
    ref.current = dependencies;
  }

  useEffect(callback, [ref.current]);
}

// 使用示例
function ComplexComponent({ config }) {
  useDeepCompareEffect(() => {
    // 处理复杂的配置对象
    initializeWithConfig(config);
  }, [config]); // 深度比较config的变化

  return <div>{/* 组件内容 */}</div>;
}

血泪教训:我踩过的坑

有一次在开发实时仪表板时,因为一个对象依赖没处理好,导致每秒发起几十次请求,差点把后端服务打挂!

jsx 复制代码
// 错误代码
useEffect(() => {
  const interval = setInterval(() => {
    fetchData(currentFilters); // currentFilters每次都是新对象
  }, 1000);
  
  return () => clearInterval(interval);
}, [currentFilters]); // 每秒都在重新创建interval!

总结一下我的经验

  1. 尽量使用基本类型作为依赖
  2. useMemo和useCallback是你的好朋友
  3. 复杂的对象依赖考虑拆解为基本类型
  4. 必要时使用深度比较,但要谨慎性能影响

记住这些技巧,下次遇到useEffect的依赖问题时,你就能像老司机一样从容应对了!

⭐ 写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

相关推荐
sorryhc22 分钟前
【AI解读源码系列】ant design mobile——Button按钮
前端·javascript·react.js
VOLUN24 分钟前
PageLayout布局组件封装技巧
前端·javascript·vue.js
掘金安东尼24 分钟前
React 的 use() API 或将取代 useContext
前端·javascript·react.js
牛马喜喜24 分钟前
记录一次el-table+sortablejs的拖拽bug
前端
一枚前端小能手29 分钟前
⚡ Vite开发体验还能更爽?这些配置你试过吗
前端·vite
anyup1 小时前
🔥 🔥 为什么我建议你使用 uView Pro 来开发 uni-app 项目?
前端·vue.js·uni-app
Skelanimals1 小时前
Elpis全栈框架开发总结
前端
蓝胖子的小叮当1 小时前
JavaScript基础(十三)函数柯里化curry
前端·javascript
孪创启航营1 小时前
数字孪生二维热力图制作,看这篇文章就够了!
前端·three.js·cesium
宫水三叶的刷题日记1 小时前
真的会玩,钉钉前脚辟谣高管凌晨巡查工位,小编随后深夜发文
前端·后端·面试