前端Tabs切换导致的数据问题
文章目录
- 1、使用唯一标识
 - 2、使用防抖
 - 3、在组件卸载时取消请求
 - 4、使用 Map 缓存每个 tab 的数据
 - 5、禁用disabled
 - 5、初始化获取所有tabs数据
 
tabs切换导致的数据请求,有可能会导致请求频率高、返回值不对应的问题,提供几种基本的方案。
1、使用唯一标识
为每次请求生成一个唯一标识(如 UUID),并在响应时检查该标识是否仍然有效。如果请求的标识不是最新的,则忽略该请求的结果。
import { useRef } from 'react';const AppDetail = () => {const [activeTab, setActiveTab] = useState<number>(0);const [dataSource, setDataSource] = useState([]);const [loading, setLoading] = useState(false);const requestRef = useRef<string | null>(null);const fetchData = async (tabKey: number) => {setLoading(true);const currentRequestId = `${tabKey}-${Date.now()}`; // 生成唯一标识requestRef.current = currentRequestId;try {const response = await fetch(`/api/data?tab=${tabKey}`);const data = await response.json();// 检查请求是否仍然有效if (requestRef.current === currentRequestId) {setDataSource(data);}} catch (error) {console.error('Error fetching data:', error);} finally {if (requestRef.current === currentRequestId) {setLoading(false);}}};const handleTabChange = (key: string) => {setActiveTab(Number(key));fetchData(Number(key));};return (<Tabs activeKey={activeTab + ''} onChange={handleTabChange}>{/* Tabs 内容 */}</Tabs>);
};
 
2、使用防抖
通过防抖机制,限制用户快速切换 Tabs 时的请求频率,避免频繁发起请求。
import { debounce } from 'lodash';const AppDetail = () => {const [activeTab, setActiveTab] = useState<number>(0);const [dataSource, setDataSource] = useState([]);const [loading, setLoading] = useState(false);const fetchData = async (tabKey: number) => {setLoading(true);try {const response = await fetch(`/api/data?tab=${tabKey}`);const data = await response.json();setDataSource(data);} catch (error) {console.error('Error fetching data:', error);} finally {setLoading(false);}};const debouncedFetchData = useCallback(debounce(fetchData, 300), []);const handleTabChange = (key: string) => {setActiveTab(Number(key));debouncedFetchData(Number(key));};return (<Tabs activeKey={activeTab + ''} onChange={handleTabChange}>{/* Tabs 内容 */}</Tabs>);
};
 
3、在组件卸载时取消请求
- 如果组件在请求完成前被卸载,可能会导致状态更新错误。可以通过 AbortController 或 isMounted 标志来取消未完成的请求。
 - AbortController 是在同一个接口请求时取消之前的请求,可以封装在请求函数里,这里只做模拟展示(isCanceler)
 
import { useEffect, useRef } from 'react';const AppDetail = () => {const [activeTab, setActiveTab] = useState<number>(0);const [dataSource, setDataSource] = useState([]);const [loading, setLoading] = useState(false);const isMounted = useRef(true);useEffect(() => {isMounted.current = true;return () => {isMounted.current = false;};}, []);const fetchData = async (tabKey: number) => {setLoading(true);try {const [response, isCanceler] = await fetch(`/api/data?tab=${tabKey}`);const data = await response.json();if (isMounted.current && !isCanceler) {setDataSource(data);}} catch (error) {console.error('Error fetching data:', error);} finally {if (isMounted.current) {setLoading(false);}}};const handleTabChange = (key: string) => {setActiveTab(Number(key));fetchData(Number(key));};return (<Tabs activeKey={activeTab + ''} onChange={handleTabChange}>{/* Tabs 内容 */}</Tabs>);
};
 
4、使用 Map 缓存每个 tab 的数据
- 通过 Map 数据结构缓存每个 tab 的数据,避免重复请求并解决 tab 切换时数据请求异常的问题。
 - 这里使用了缓存数据就会存在数据实时性的问题。
 
import React, { useState, useRef, useCallback } from 'react';
import { Tabs, Table, Spin } from 'antd';const fetchTabData = async (tabKey) => {// 模拟 API 请求return new Promise((resolve) => {setTimeout(() => {resolve({ data: [`Data for tab ${tabKey}`], key: tabKey });}, 1000);});
};const TabsContainer = () => {const [activeTab, setActiveTab] = useState('tab1'); // 当前激活的 tabconst [loading, setLoading] = useState(false); // 加载状态const [dataSource, setDataSource] = useState([]); // 当前 tab 的数据const dataCache = useRef(new Map()); // 使用 Map 缓存每个 tab 的数据// 获取数据const getTabData = useCallback(async (tabKey) => {// 如果缓存中有数据,直接使用缓存数据if (dataCache.current.has(tabKey)) {setDataSource(dataCache.current.get(tabKey));return;}// 如果缓存中没有数据,发起请求setLoading(true);try {const response = await fetchTabData(tabKey);const data = response.data;// 更新缓存dataCache.current.set(tabKey, data);// 更新当前数据setDataSource(data);} catch (error) {console.error(`Error fetching data for tab ${tabKey}:`, error);} finally {setLoading(false);}},[],);// 处理 tab 切换const handleTabChange = (key) => {setActiveTab(key);getTabData(key); // 切换 tab 时获取数据};return (<div><Tabs activeKey={activeTab} onChange={handleTabChange}><Tabs.TabPane tab="Tab 1" key="tab1" /><Tabs.TabPane tab="Tab 2" key="tab2" /><Tabs.TabPane tab="Tab 3" key="tab3" /></Tabs><Spin spinning={loading}><TablerowKey={(record) => record}dataSource={dataSource}columns={[{ title: 'Data', dataIndex: 'data', key: 'data' }]}pagination={false}/></Spin></div>);
};export default TabsContainer;
 
5、禁用disabled
简单有效,通过在请求数据时禁用整个 Tabs,可以有效避免用户在请求未完成时切换 Tabs。
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { Tabs, Table, Spin } from 'antd';const fetchTabData = async (tabKey) => {// 模拟 API 请求return new Promise((resolve) => {setTimeout(() => {resolve({ data: [`Data for tab ${tabKey}`], key: tabKey });}, 1000);});
};const AppDetail = () => {const [activeTab, setActiveTab] = useState('tab1'); // 当前激活的 tabconst [loading, setLoading] = useState(false); // 加载状态const [dataSource, setDataSource] = useState([]); // 当前 tab 的数据// 获取数据const getTabData = useCallback(async (tabKey) => {setLoading(true); // 开始加载,禁用 Tabstry {const response = await fetchTabData(tabKey);const data = response.data;// 更新当前数据setDataSource(data);} catch (error) {console.error(`Error fetching data for tab ${tabKey}:`, error);} finally {setLoading(false); // 加载完成,启用 Tabs}}, []);// 处理 tab 切换const handleTabChange = (key) => {setActiveTab(key);getTabData(key); // 切换 tab 时获取数据};return (<div>{/* Tabs 区域 */}<Spin spinning={loading}><TabsactiveKey={activeTab}onChange={handleTabChange}tabBarStyle={loading ? { pointerEvents: 'none', opacity: 0.5 } : {}}><Tabs.TabPane tab="Tab 1" key="tab1" /><Tabs.TabPane tab="Tab 2" key="tab2" /><Tabs.TabPane tab="Tab 3" key="tab3" /></Tabs></Spin>{/* 表格区域 */}<TablerowKey={(record) => record}dataSource={dataSource}columns={[{ title: 'Data', dataIndex: 'data', key: 'data' }]}pagination={false}/></div>);
};export default AppDetail;
 
5、初始化获取所有tabs数据
- 组件加载时默认请求所有 Tabs 的数据并缓存起来,切换 Tabs 时只切换数据,而不发起新的请求
 - 如果接口返回的就是所有tabs的数据,那一次请求即可,适合数据量小的情况;
 - 对于初始化一次就不再更新的数据,一般属于固定的常量数据、实时性低的数据。
 
import React, { useState, useEffect, useMemo } from 'react';
import { Tabs, Table, Spin } from 'antd';// 模拟 API 请求
const fetchTabData = async (tabKey: string) => {return new Promise((resolve) => {setTimeout(() => {resolve(Array.from({ length: 5 }, (_, index) => ({id: `${tabKey}-${index}`,name: `Item ${index + 1} for ${tabKey}`,})),);}, 1000);});
};// Tabs 配置
const getTabsConfig = () => [{ key: 'tab1', label: 'Tab 1' },{ key: 'tab2', label: 'Tab 2' },{ key: 'tab3', label: 'Tab 3' },
];const App = () => {const [activeTab, setActiveTab] = useState<string>(null); // 当前激活的 Tabconst [loading, setLoading] = useState<boolean>(true); // 全局加载状态const [dataCache, setDataCache] = useState<Record<string, any[]>>({}); // 缓存所有 Tab 的数据const tabs = useMemo(() => getTabsConfig(), []); // 获取 Tabs 配置// 默认请求所有 Tabs 的数据useEffect(() => {const fetchAllTabsData = async () => {setLoading(true);const cache: Record<string, any[]> = {};try {// 并发请求所有 Tabs 的数据const requests = tabs.map((tab) =>fetchTabData(tab.key).then((data) => {cache[tab.key] = data;}),);await Promise.all(requests); // 等待所有请求完成setDataCache(cache); // 缓存所有数据setActiveTab(tabs[0]?.key); // 默认激活第一个 Tab} catch (error) {console.error('Error fetching tabs data:', error);} finally {setLoading(false);}};fetchAllTabsData();}, [tabs]);// 切换 Tabconst handleTabChange = (key: string) => {setActiveTab(key);};return (<div style={{ padding: 20 }}>{/* 全局加载指示器 */}<Spin spinning={loading}>{/* Tabs 区域 */}<Tabs activeKey={activeTab} onChange={handleTabChange}>{tabs.map((tab) => (<Tabs.TabPane tab={tab.label} key={tab.key} />))}</Tabs>{/* 表格区域 */}{activeTab && (<TablerowKey={(record) => record.id}dataSource={dataCache[activeTab] || []} // 从缓存中读取数据columns={[{ title: 'ID', dataIndex: 'id', key: 'id' },{ title: 'Name', dataIndex: 'name', key: 'name' },]}pagination={false}/>)}</Spin></div>);
};export default App;
