防抖后滚动事件仍多次触发,分页加载API调用异常排查
滚动触底加载组件的API重复调用问题
我开发了一个滚动触底加载更多数据的React组件,每次加载12张图片,但目前每次滚动会触发约10次API调用,远超预期。存在两个关联问题:
- 初始加载时API调用page1正常,但首次滚动时仍调用page1(初始加载后page已设为2),后续滚动加载正常;
- 已使用防抖和isLoading状态控制,为何仍多次触发API调用?
组件代码
import debounce from 'lodash.debounce'; import useStore from "../utils/store.js"; // zustand store import React, {useEffect, useState} from "react"; import {fetchItems} from "../utils/rest.js"; import MyComponent from "./MyComponent.jsx"; const Layout = () => { const setItems = useStore((state) => state.setItems); const page = useStore((state) => state.page); const setPage = useStore((state) => state.setPage); const [isLoading, setIsLoading] = useState(false); // hook or zustand state, same outcome const debouncedFetchItems = debounce(() => fetchItems(page), 50); const updatePageAndItems = (newItems) => { setPage(); setItems(newItems); }; const handleScroll = () => { const offset = 1000; if (!isLoading && window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - offset) { setIsLoading(true); const promise = debouncedFetchItems(); promise?.then(newItems => { updatePageAndItems(newItems) setIsLoading(false) }); } }; useEffect(() => { fetchItems(page).then(newItems => updatePageAndItems(newItems)); }, []); useEffect(() => { window.addEventListener('scroll', handleScroll); },[page]); return ( <MyComponent /> {/* This component loads data based on using the global state value from items */} ); }; export default Layout;
Store代码
import { create } from "zustand"; import { combine } from 'zustand/middleware' const initialValues = { items: [], page: 1, } const useStore = create( combine( { ...initialValues }, (set) => ({ setItems: (newItems) => set((state) => ({ items: [...state.items, ...newItems] })), setPage: () => set((state) => ({ page: state.page + 1 })), }) ), ) export default useStore;
注:尝试过无依赖的useEffect配置,不会重复触发API,但page始终为1;使用page作为依赖时能获取更新后的page,但会多次触发API。更新后的useEffect配置如下:
useEffect(() => { window.addEventListener('scroll', handleScroll); return () => { window.removeEventListener('scroll', handleScroll); } }, [page]);
问题分析与修复方案
核心问题原因
- 闭包导致首次滚动page值错误:
debouncedFetchItems在组件初始化时创建,捕获了初始page=1的闭包值,即便后续page更新为2,首次滚动调用的还是旧的防抖函数,使用的依然是page=1。 - 防抖失效引发多次调用:每次组件渲染(比如page变化时)都会重新创建新的
debouncedFetchItems实例,原来的防抖逻辑失效;同时handleScroll没有用useCallback包裹,每次page变化会绑定新的滚动事件,旧的事件处理器可能未被正确清理,导致多个处理器同时触发。 - 滚动事件绑定时机错误:将
page作为滚动事件的依赖,每次page更新都会重新绑定滚动事件,而handleScroll内部的逻辑没有做好防抖控制,进一步加剧了重复调用。
修复后的代码
import debounce from 'lodash.debounce'; import useStore from "../utils/store.js"; import React, {useEffect, useState, useCallback, useMemo} from "react"; import {fetchItems} from "../utils/rest.js"; import MyComponent from "./MyComponent.jsx"; const Layout = () => { const setItems = useStore((state) => state.setItems); const page = useStore((state) => state.page); const setPage = useStore((state) => state.setPage); const [isLoading, setIsLoading] = useState(false); // 用useMemo确保page变化时更新防抖函数,捕获最新page值 const debouncedFetchItems = useMemo(() => { return debounce(async () => { const newItems = await fetchItems(page); updatePageAndItems(newItems); setIsLoading(false); }, 150); // 延长防抖时间,避免快速滚动触发 }, [page]); const updatePageAndItems = useCallback((newItems) => { setPage(); setItems(newItems); }, [setPage, setItems]); // 用useCallback包裹handleScroll,避免不必要的重新创建 const handleScroll = useCallback(() => { const offset = 1000; if (!isLoading && window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - offset) { setIsLoading(true); debouncedFetchItems(); } }, [isLoading, debouncedFetchItems]); // 初始加载:只在组件挂载时执行一次 useEffect(() => { const loadInitialData = async () => { const newItems = await fetchItems(page); updatePageAndItems(newItems); }; loadInitialData(); }, []); // 滚动事件监听:只在组件挂载时绑定一次,依赖handleScroll(useCallback确保稳定) useEffect(() => { window.addEventListener('scroll', handleScroll); return () => { window.removeEventListener('scroll', handleScroll); // 清理防抖定时器,避免组件卸载后仍执行 debouncedFetchItems.cancel(); }; }, [handleScroll]); return ( <MyComponent /> ); }; export default Layout;
关键修复点说明
- 用
useMemo创建防抖函数:依赖page,确保page更新时生成新的防抖函数,捕获最新的page值,解决首次滚动page错误的问题。 handleScroll用useCallback包裹:依赖isLoading和debouncedFetchItems,确保函数引用稳定,避免滚动事件重复绑定。- 调整防抖时间:从50ms延长到150ms,更符合滚动场景的防抖需求,减少触发次数。
- 清理防抖定时器:在组件卸载时调用
debouncedFetchItems.cancel(),避免内存泄漏和不必要的API调用。 - 滚动事件仅绑定一次:不再依赖
page,而是依赖稳定的handleScroll引用,避免每次page变化都重新绑定事件。
内容的提问来源于stack exchange,提问作者kar
相关产品推荐
相关产品推荐

