perf(hooks): 使用 useRef 缓存加载状态,避免 Tab 切换重复请求

- 使用 useRef 替代 useState 跟踪 hasLoaded 状态
- Tab 切换回来时保持数据缓存,不重新发起请求
- stockCode 变化时重置加载状态,确保新股票正常加载
- useAnnouncementsData 支持 refreshKey 强制刷新

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-22 13:02:29 +08:00
parent 9e271747da
commit 77ea38e5c9
5 changed files with 109 additions and 25 deletions

View File

@@ -1,7 +1,7 @@
// src/views/Company/components/CompanyOverview/hooks/useAnnouncementsData.ts // src/views/Company/components/CompanyOverview/hooks/useAnnouncementsData.ts
// 公告数据 Hook - 用于公司公告 Tab // 公告数据 Hook - 用于公司公告 Tab
import { useState, useEffect } from "react"; import { useState, useEffect, useRef } from "react";
import { logger } from "@utils/logger"; import { logger } from "@utils/logger";
import axios from "@utils/axiosConfig"; import axios from "@utils/axiosConfig";
import type { Announcement } from "../types"; import type { Announcement } from "../types";
@@ -39,7 +39,11 @@ export const useAnnouncementsData = (options: UseAnnouncementsDataOptions): UseA
const [announcements, setAnnouncements] = useState<Announcement[]>([]); const [announcements, setAnnouncements] = useState<Announcement[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [hasLoaded, setHasLoaded] = useState(false); // 使用 ref 跟踪是否已加载,避免 Tab 切换时重复请求
const hasLoadedRef = useRef(false);
// 记录上次加载的 stockCode 和 refreshKey
const lastStockCodeRef = useRef<string | undefined>(undefined);
const lastRefreshKeyRef = useRef<number | undefined>(undefined);
useEffect(() => { useEffect(() => {
// 只有 enabled 且有 stockCode 时才请求 // 只有 enabled 且有 stockCode 时才请求
@@ -48,6 +52,26 @@ export const useAnnouncementsData = (options: UseAnnouncementsDataOptions): UseA
return; return;
} }
// stockCode 或 refreshKey 变化时重置加载状态
if (lastStockCodeRef.current !== stockCode || lastRefreshKeyRef.current !== refreshKey) {
// refreshKey 变化时强制重新加载
if (lastRefreshKeyRef.current !== refreshKey && lastRefreshKeyRef.current !== undefined) {
hasLoadedRef.current = false;
}
// stockCode 变化时重置
if (lastStockCodeRef.current !== stockCode) {
hasLoadedRef.current = false;
}
lastStockCodeRef.current = stockCode;
lastRefreshKeyRef.current = refreshKey;
}
// 如果已经加载过数据不再重新请求Tab 切换回来时保持缓存)
if (hasLoadedRef.current) {
setLoading(false);
return;
}
const controller = new AbortController(); const controller = new AbortController();
const loadData = async () => { const loadData = async () => {
@@ -66,7 +90,7 @@ export const useAnnouncementsData = (options: UseAnnouncementsDataOptions): UseA
setError("加载公告数据失败"); setError("加载公告数据失败");
} }
setLoading(false); setLoading(false);
setHasLoaded(true); hasLoadedRef.current = true;
} catch (err: any) { } catch (err: any) {
// 请求被取消时,不更新任何状态 // 请求被取消时,不更新任何状态
if (err.name === "CanceledError") { if (err.name === "CanceledError") {
@@ -75,7 +99,7 @@ export const useAnnouncementsData = (options: UseAnnouncementsDataOptions): UseA
logger.error("useAnnouncementsData", "loadData", err, { stockCode }); logger.error("useAnnouncementsData", "loadData", err, { stockCode });
setError("网络请求失败"); setError("网络请求失败");
setLoading(false); setLoading(false);
setHasLoaded(true); hasLoadedRef.current = true;
} }
}; };
@@ -83,7 +107,7 @@ export const useAnnouncementsData = (options: UseAnnouncementsDataOptions): UseA
return () => controller.abort(); return () => controller.abort();
}, [stockCode, enabled, refreshKey]); }, [stockCode, enabled, refreshKey]);
const isLoading = loading || (enabled && !hasLoaded && !error); const isLoading = loading || (enabled && !hasLoadedRef.current && !error);
return { announcements, loading: isLoading, error }; return { announcements, loading: isLoading, error };
}; };

View File

@@ -1,7 +1,7 @@
// src/views/Company/components/CompanyOverview/hooks/useBranchesData.ts // src/views/Company/components/CompanyOverview/hooks/useBranchesData.ts
// 分支机构数据 Hook - 用于分支机构 Tab // 分支机构数据 Hook - 用于分支机构 Tab
import { useState, useEffect } from "react"; import { useState, useEffect, useRef } from "react";
import { logger } from "@utils/logger"; import { logger } from "@utils/logger";
import axios from "@utils/axiosConfig"; import axios from "@utils/axiosConfig";
import type { Branch } from "../types"; import type { Branch } from "../types";
@@ -36,7 +36,10 @@ export const useBranchesData = (options: UseBranchesDataOptions): UseBranchesDat
const [branches, setBranches] = useState<Branch[]>([]); const [branches, setBranches] = useState<Branch[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [hasLoaded, setHasLoaded] = useState(false); // 使用 ref 跟踪是否已加载,避免 Tab 切换时重复请求
const hasLoadedRef = useRef(false);
// 记录上次加载的 stockCodestockCode 变化时需要重新加载
const lastStockCodeRef = useRef<string | undefined>(undefined);
useEffect(() => { useEffect(() => {
if (!enabled || !stockCode) { if (!enabled || !stockCode) {
@@ -44,6 +47,18 @@ export const useBranchesData = (options: UseBranchesDataOptions): UseBranchesDat
return; return;
} }
// stockCode 变化时重置加载状态
if (lastStockCodeRef.current !== stockCode) {
hasLoadedRef.current = false;
lastStockCodeRef.current = stockCode;
}
// 如果已经加载过数据不再重新请求Tab 切换回来时保持缓存)
if (hasLoadedRef.current) {
setLoading(false);
return;
}
const controller = new AbortController(); const controller = new AbortController();
const loadData = async () => { const loadData = async () => {
@@ -62,7 +77,7 @@ export const useBranchesData = (options: UseBranchesDataOptions): UseBranchesDat
setError("加载分支机构数据失败"); setError("加载分支机构数据失败");
} }
setLoading(false); setLoading(false);
setHasLoaded(true); hasLoadedRef.current = true;
} catch (err: any) { } catch (err: any) {
// 请求被取消时,不更新任何状态 // 请求被取消时,不更新任何状态
if (err.name === "CanceledError") { if (err.name === "CanceledError") {
@@ -71,7 +86,7 @@ export const useBranchesData = (options: UseBranchesDataOptions): UseBranchesDat
logger.error("useBranchesData", "loadData", err, { stockCode }); logger.error("useBranchesData", "loadData", err, { stockCode });
setError("网络请求失败"); setError("网络请求失败");
setLoading(false); setLoading(false);
setHasLoaded(true); hasLoadedRef.current = true;
} }
}; };
@@ -79,7 +94,7 @@ export const useBranchesData = (options: UseBranchesDataOptions): UseBranchesDat
return () => controller.abort(); return () => controller.abort();
}, [stockCode, enabled]); }, [stockCode, enabled]);
const isLoading = loading || (enabled && !hasLoaded && !error); const isLoading = loading || (enabled && !hasLoadedRef.current && !error);
return { branches, loading: isLoading, error }; return { branches, loading: isLoading, error };
}; };

View File

@@ -1,7 +1,7 @@
// src/views/Company/components/CompanyOverview/hooks/useDisclosureData.ts // src/views/Company/components/CompanyOverview/hooks/useDisclosureData.ts
// 披露日程数据 Hook - 用于工商信息 Tab // 披露日程数据 Hook - 用于工商信息 Tab
import { useState, useEffect } from "react"; import { useState, useEffect, useRef } from "react";
import { logger } from "@utils/logger"; import { logger } from "@utils/logger";
import axios from "@utils/axiosConfig"; import axios from "@utils/axiosConfig";
import type { DisclosureSchedule } from "../types"; import type { DisclosureSchedule } from "../types";
@@ -36,7 +36,10 @@ export const useDisclosureData = (options: UseDisclosureDataOptions): UseDisclos
const [disclosureSchedule, setDisclosureSchedule] = useState<DisclosureSchedule[]>([]); const [disclosureSchedule, setDisclosureSchedule] = useState<DisclosureSchedule[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [hasLoaded, setHasLoaded] = useState(false); // 使用 ref 跟踪是否已加载,避免 Tab 切换时重复请求
const hasLoadedRef = useRef(false);
// 记录上次加载的 stockCodestockCode 变化时需要重新加载
const lastStockCodeRef = useRef<string | undefined>(undefined);
useEffect(() => { useEffect(() => {
// 只有 enabled 且有 stockCode 时才请求 // 只有 enabled 且有 stockCode 时才请求
@@ -45,6 +48,18 @@ export const useDisclosureData = (options: UseDisclosureDataOptions): UseDisclos
return; return;
} }
// stockCode 变化时重置加载状态
if (lastStockCodeRef.current !== stockCode) {
hasLoadedRef.current = false;
lastStockCodeRef.current = stockCode;
}
// 如果已经加载过数据不再重新请求Tab 切换回来时保持缓存)
if (hasLoadedRef.current) {
setLoading(false);
return;
}
const controller = new AbortController(); const controller = new AbortController();
const loadData = async () => { const loadData = async () => {
@@ -63,7 +78,7 @@ export const useDisclosureData = (options: UseDisclosureDataOptions): UseDisclos
setError("加载披露日程数据失败"); setError("加载披露日程数据失败");
} }
setLoading(false); setLoading(false);
setHasLoaded(true); hasLoadedRef.current = true;
} catch (err: any) { } catch (err: any) {
// 请求被取消时,不更新任何状态 // 请求被取消时,不更新任何状态
if (err.name === "CanceledError") { if (err.name === "CanceledError") {
@@ -72,7 +87,7 @@ export const useDisclosureData = (options: UseDisclosureDataOptions): UseDisclos
logger.error("useDisclosureData", "loadData", err, { stockCode }); logger.error("useDisclosureData", "loadData", err, { stockCode });
setError("网络请求失败"); setError("网络请求失败");
setLoading(false); setLoading(false);
setHasLoaded(true); hasLoadedRef.current = true;
} }
}; };
@@ -80,7 +95,7 @@ export const useDisclosureData = (options: UseDisclosureDataOptions): UseDisclos
return () => controller.abort(); return () => controller.abort();
}, [stockCode, enabled]); }, [stockCode, enabled]);
const isLoading = loading || (enabled && !hasLoaded && !error); const isLoading = loading || (enabled && !hasLoadedRef.current && !error);
return { disclosureSchedule, loading: isLoading, error }; return { disclosureSchedule, loading: isLoading, error };
}; };

View File

@@ -1,7 +1,7 @@
// src/views/Company/components/CompanyOverview/hooks/useManagementData.ts // src/views/Company/components/CompanyOverview/hooks/useManagementData.ts
// 管理团队数据 Hook - 用于管理团队 Tab // 管理团队数据 Hook - 用于管理团队 Tab
import { useState, useEffect } from "react"; import { useState, useEffect, useRef } from "react";
import { logger } from "@utils/logger"; import { logger } from "@utils/logger";
import axios from "@utils/axiosConfig"; import axios from "@utils/axiosConfig";
import type { Management } from "../types"; import type { Management } from "../types";
@@ -36,7 +36,10 @@ export const useManagementData = (options: UseManagementDataOptions): UseManagem
const [management, setManagement] = useState<Management[]>([]); const [management, setManagement] = useState<Management[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [hasLoaded, setHasLoaded] = useState(false); // 使用 ref 跟踪是否已加载,避免 Tab 切换时重复请求
const hasLoadedRef = useRef(false);
// 记录上次加载的 stockCodestockCode 变化时需要重新加载
const lastStockCodeRef = useRef<string | undefined>(undefined);
useEffect(() => { useEffect(() => {
// 只有 enabled 且有 stockCode 时才请求 // 只有 enabled 且有 stockCode 时才请求
@@ -45,6 +48,18 @@ export const useManagementData = (options: UseManagementDataOptions): UseManagem
return; return;
} }
// stockCode 变化时重置加载状态
if (lastStockCodeRef.current !== stockCode) {
hasLoadedRef.current = false;
lastStockCodeRef.current = stockCode;
}
// 如果已经加载过数据不再重新请求Tab 切换回来时保持缓存)
if (hasLoadedRef.current) {
setLoading(false);
return;
}
const controller = new AbortController(); const controller = new AbortController();
const loadData = async () => { const loadData = async () => {
@@ -63,7 +78,7 @@ export const useManagementData = (options: UseManagementDataOptions): UseManagem
setError("加载管理团队数据失败"); setError("加载管理团队数据失败");
} }
setLoading(false); setLoading(false);
setHasLoaded(true); hasLoadedRef.current = true;
} catch (err: any) { } catch (err: any) {
// 请求被取消时,不更新任何状态 // 请求被取消时,不更新任何状态
if (err.name === "CanceledError") { if (err.name === "CanceledError") {
@@ -72,7 +87,7 @@ export const useManagementData = (options: UseManagementDataOptions): UseManagem
logger.error("useManagementData", "loadData", err, { stockCode }); logger.error("useManagementData", "loadData", err, { stockCode });
setError("网络请求失败"); setError("网络请求失败");
setLoading(false); setLoading(false);
setHasLoaded(true); hasLoadedRef.current = true;
} }
}; };
@@ -82,7 +97,7 @@ export const useManagementData = (options: UseManagementDataOptions): UseManagem
// 派生 loading 状态enabled 但尚未完成首次加载时,视为 loading // 派生 loading 状态enabled 但尚未完成首次加载时,视为 loading
// 这样可以在渲染时同步判断,避免 useEffect 异步导致的空状态闪烁 // 这样可以在渲染时同步判断,避免 useEffect 异步导致的空状态闪烁
const isLoading = loading || (enabled && !hasLoaded && !error); const isLoading = loading || (enabled && !hasLoadedRef.current && !error);
return { management, loading: isLoading, error }; return { management, loading: isLoading, error };
}; };

View File

@@ -1,7 +1,7 @@
// src/views/Company/components/CompanyOverview/hooks/useShareholderData.ts // src/views/Company/components/CompanyOverview/hooks/useShareholderData.ts
// 股权结构数据 Hook - 用于股权结构 Tab // 股权结构数据 Hook - 用于股权结构 Tab
import { useState, useEffect } from "react"; import { useState, useEffect, useRef } from "react";
import { logger } from "@utils/logger"; import { logger } from "@utils/logger";
import axios from "@utils/axiosConfig"; import axios from "@utils/axiosConfig";
import type { ActualControl, Concentration, Shareholder } from "../types"; import type { ActualControl, Concentration, Shareholder } from "../types";
@@ -42,7 +42,10 @@ export const useShareholderData = (options: UseShareholderDataOptions): UseShare
const [topCirculationShareholders, setTopCirculationShareholders] = useState<Shareholder[]>([]); const [topCirculationShareholders, setTopCirculationShareholders] = useState<Shareholder[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [hasLoaded, setHasLoaded] = useState(false); // 使用 ref 跟踪是否已加载,避免 Tab 切换时重复请求
const hasLoadedRef = useRef(false);
// 记录上次加载的 stockCodestockCode 变化时需要重新加载
const lastStockCodeRef = useRef<string | undefined>(undefined);
useEffect(() => { useEffect(() => {
// 只有 enabled 且有 stockCode 时才请求 // 只有 enabled 且有 stockCode 时才请求
@@ -51,6 +54,18 @@ export const useShareholderData = (options: UseShareholderDataOptions): UseShare
return; return;
} }
// stockCode 变化时重置加载状态
if (lastStockCodeRef.current !== stockCode) {
hasLoadedRef.current = false;
lastStockCodeRef.current = stockCode;
}
// 如果已经加载过数据不再重新请求Tab 切换回来时保持缓存)
if (hasLoadedRef.current) {
setLoading(false);
return;
}
const controller = new AbortController(); const controller = new AbortController();
const loadData = async () => { const loadData = async () => {
@@ -75,7 +90,7 @@ export const useShareholderData = (options: UseShareholderDataOptions): UseShare
if (shareholdersRes.success) setTopShareholders(shareholdersRes.data); if (shareholdersRes.success) setTopShareholders(shareholdersRes.data);
if (circulationRes.success) setTopCirculationShareholders(circulationRes.data); if (circulationRes.success) setTopCirculationShareholders(circulationRes.data);
setLoading(false); setLoading(false);
setHasLoaded(true); hasLoadedRef.current = true;
} catch (err: any) { } catch (err: any) {
// 请求被取消时,不更新任何状态 // 请求被取消时,不更新任何状态
if (err.name === "CanceledError") { if (err.name === "CanceledError") {
@@ -84,7 +99,7 @@ export const useShareholderData = (options: UseShareholderDataOptions): UseShare
logger.error("useShareholderData", "loadData", err, { stockCode }); logger.error("useShareholderData", "loadData", err, { stockCode });
setError("加载股权结构数据失败"); setError("加载股权结构数据失败");
setLoading(false); setLoading(false);
setHasLoaded(true); hasLoadedRef.current = true;
} }
}; };
@@ -92,7 +107,7 @@ export const useShareholderData = (options: UseShareholderDataOptions): UseShare
return () => controller.abort(); return () => controller.abort();
}, [stockCode, enabled]); }, [stockCode, enabled]);
const isLoading = loading || (enabled && !hasLoaded && !error); const isLoading = loading || (enabled && !hasLoadedRef.current && !error);
return { return {
actualControl, actualControl,