feat: user 依赖优化

This commit is contained in:
zdl
2025-10-24 12:19:37 +08:00
parent 5eb4227e29
commit e91656d332
3 changed files with 97 additions and 57 deletions

View File

@@ -489,6 +489,11 @@ export default function HomeNavbar() {
const brandHover = useColorModeValue('blue.600', 'blue.300'); const brandHover = useColorModeValue('blue.600', 'blue.300');
const toast = useToast(); const toast = useToast();
// ⚡ 提取 userId 为独立变量,避免 user 对象引用变化导致无限循环
const userId = user?.id;
const prevUserIdRef = React.useRef(userId);
const prevIsAuthenticatedRef = React.useRef(isAuthenticated);
// 添加调试信息 // 添加调试信息
logger.debug('HomeNavbar', '组件渲染状态', { logger.debug('HomeNavbar', '组件渲染状态', {
hasUser: !!user, hasUser: !!user,
@@ -727,65 +732,81 @@ export default function HomeNavbar() {
error: error.message error: error.message
}); });
} }
}, [isAuthenticated, user?.id]); // 只依赖 user.id,避免 user 对象变化导致无限循环 }, [isAuthenticated, userId]); // ⚡ 使用 userId 而不是 user?.id
// 监听用户变化,重置检查标志(用户切换或退出登录时) // 监听用户变化,重置检查标志(用户切换或退出登录时)
React.useEffect(() => { React.useEffect(() => {
if (!isAuthenticated || !user) { const userIdChanged = prevUserIdRef.current !== userId;
// 用户退出登录,重置标志 const authChanged = prevIsAuthenticatedRef.current !== isAuthenticated;
hasCheckedCompleteness.current = false;
setProfileCompleteness(null); if (userIdChanged || authChanged) {
setShowCompletenessAlert(false); prevUserIdRef.current = userId;
prevIsAuthenticatedRef.current = isAuthenticated;
if (!isAuthenticated || !user) {
// 用户退出登录,重置标志
hasCheckedCompleteness.current = false;
setProfileCompleteness(null);
setShowCompletenessAlert(false);
}
} }
}, [isAuthenticated, user?.id]); // 监听用户 ID 变化 }, [isAuthenticated, userId, user]); // ⚡ 使用 userId
// 用户登录后检查资料完整性 // 用户登录后检查资料完整性
React.useEffect(() => { React.useEffect(() => {
if (isAuthenticated && user) { const userIdChanged = prevUserIdRef.current !== userId;
const authChanged = prevIsAuthenticatedRef.current !== isAuthenticated;
if ((userIdChanged || authChanged) && isAuthenticated && user) {
// 延迟检查,避免过于频繁 // 延迟检查,避免过于频繁
const timer = setTimeout(checkProfileCompleteness, 1000); const timer = setTimeout(checkProfileCompleteness, 1000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [isAuthenticated, user?.id, checkProfileCompleteness]); // 只依赖 user.id,避免无限循环 }, [isAuthenticated, userId, checkProfileCompleteness, user]); // ⚡ 使用 userId
// 加载订阅信息 // 加载订阅信息
React.useEffect(() => { React.useEffect(() => {
if (isAuthenticated && user) { const userIdChanged = prevUserIdRef.current !== userId;
const loadSubscriptionInfo = async () => { const authChanged = prevIsAuthenticatedRef.current !== isAuthenticated;
try {
const base = getApiBase(); if (userIdChanged || authChanged) {
const response = await fetch(base + '/api/subscription/current', { if (isAuthenticated && user) {
credentials: 'include', const loadSubscriptionInfo = async () => {
}); try {
if (response.ok) { const base = getApiBase();
const data = await response.json(); const response = await fetch(base + '/api/subscription/current', {
if (data.success && data.data) { credentials: 'include',
// 数据标准化处理确保type字段是小写的 'free', 'pro', 或 'max' });
const normalizedData = { if (response.ok) {
type: (data.data.type || data.data.subscription_type || 'free').toLowerCase(), const data = await response.json();
status: data.data.status || 'active', if (data.success && data.data) {
days_left: data.data.days_left || 0, // 数据标准化处理确保type字段是小写的 'free', 'pro', 或 'max'
is_active: data.data.is_active !== false, const normalizedData = {
end_date: data.data.end_date || null type: (data.data.type || data.data.subscription_type || 'free').toLowerCase(),
}; status: data.data.status || 'active',
setSubscriptionInfo(normalizedData); days_left: data.data.days_left || 0,
is_active: data.data.is_active !== false,
end_date: data.data.end_date || null
};
setSubscriptionInfo(normalizedData);
}
} }
} catch (error) {
logger.error('HomeNavbar', '加载订阅信息失败', error);
} }
} catch (error) { };
logger.error('HomeNavbar', '加载订阅信息失败', error); loadSubscriptionInfo();
} } else {
}; // 用户未登录时,重置为免费版
loadSubscriptionInfo(); setSubscriptionInfo({
} else { type: 'free',
// 用户未登录时,重置为免费版 status: 'active',
setSubscriptionInfo({ days_left: 0,
type: 'free', is_active: true
status: 'active', });
days_left: 0, }
is_active: true
});
} }
}, [isAuthenticated, user?.id]); // 只依赖 user.id 而不是整个 user 对象 }, [isAuthenticated, userId, user]); // ⚡ 使用 userId防重复通过 ref 判断
return ( return (
<> <>

View File

@@ -1,5 +1,5 @@
// src/components/ProtectedRoute.js - 弹窗拦截版本 // src/components/ProtectedRoute.js - 弹窗拦截版本
import React, { useEffect } from 'react'; import React, { useEffect, useRef } from 'react';
import { Box, VStack, Spinner, Text } from '@chakra-ui/react'; import { Box, VStack, Spinner, Text } from '@chakra-ui/react';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { useAuthModal } from '../contexts/AuthModalContext'; import { useAuthModal } from '../contexts/AuthModalContext';
@@ -8,15 +8,17 @@ const ProtectedRoute = ({ children }) => {
const { isAuthenticated, isLoading, user } = useAuth(); const { isAuthenticated, isLoading, user } = useAuth();
const { openAuthModal, isAuthModalOpen } = useAuthModal(); const { openAuthModal, isAuthModalOpen } = useAuthModal();
// 记录当前路径,登录成功后可以跳转回来 // ⚡ 使用 useRef 保存当前路径,避免每次渲染创建新字符串导致 useEffect 无限循环
const currentPath = window.location.pathname + window.location.search; const currentPathRef = useRef(window.location.pathname + window.location.search);
// 未登录时自动弹出认证窗口 // 未登录时自动弹出认证窗口
useEffect(() => { useEffect(() => {
if (!isLoading && !isAuthenticated && !user && !isAuthModalOpen) { if (!isLoading && !isAuthenticated && !user && !isAuthModalOpen) {
openAuthModal(currentPath); openAuthModal(currentPathRef.current);
} }
}, [isAuthenticated, user, isLoading, isAuthModalOpen, currentPath, openAuthModal]); // ⚠️ 移除 user 依赖,因为 user 对象每次从 API 返回都是新引用,会导致无限循环
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAuthenticated, isLoading, isAuthModalOpen, openAuthModal]);
// 显示加载状态 // 显示加载状态
if (isLoading) { if (isLoading) {

View File

@@ -26,6 +26,9 @@ export const AuthProvider = ({ children }) => {
const toast = useToast(); const toast = useToast();
const { showWelcomeGuide } = useNotification(); const { showWelcomeGuide } = useNotification();
// ⚡ 使用 ref 保存最新的 isAuthenticated 值,避免事件监听器重复注册
const isAuthenticatedRef = React.useRef(isAuthenticated);
// 检查Session状态 // 检查Session状态
const checkSession = async () => { const checkSession = async () => {
try { try {
@@ -57,19 +60,27 @@ export const AuthProvider = ({ children }) => {
}); });
if (data.isAuthenticated && data.user) { if (data.isAuthenticated && data.user) {
setUser(data.user); // ⚡ 只在 user 数据真正变化时才更新状态,避免无限循环
setIsAuthenticated(true); setUser((prevUser) => {
// 比较用户 ID如果相同则不更新
if (prevUser && prevUser.id === data.user.id) {
return prevUser;
}
return data.user;
});
setIsAuthenticated((prev) => prev === true ? prev : true);
} else { } else {
setUser(null); setUser((prev) => prev === null ? prev : null);
setIsAuthenticated(false); setIsAuthenticated((prev) => prev === false ? prev : false);
} }
} catch (error) { } catch (error) {
logger.error('AuthContext', 'checkSession', error); logger.error('AuthContext', 'checkSession', error);
// 网络错误或超时,设置为未登录状态 // 网络错误或超时,设置为未登录状态
setUser(null); setUser((prev) => prev === null ? prev : null);
setIsAuthenticated(false); setIsAuthenticated((prev) => prev === false ? prev : false);
} finally { } finally {
setIsLoading(false); // ⚡ 只在 isLoading 为 true 时才设置为 false避免不必要的状态更新
setIsLoading((prev) => prev === false ? prev : false);
} }
}; };
@@ -79,11 +90,17 @@ export const AuthProvider = ({ children }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
// ⚡ 同步 isAuthenticated 到 ref
useEffect(() => {
isAuthenticatedRef.current = isAuthenticated;
}, [isAuthenticated]);
// 监听路由变化检查session处理微信登录回调 // 监听路由变化检查session处理微信登录回调
// ⚡ 移除 isAuthenticated 依赖,使用 ref 避免重复注册事件监听器
useEffect(() => { useEffect(() => {
const handleRouteChange = () => { const handleRouteChange = () => {
// 如果是从微信回调返回的重新检查session // 使用 ref 获取最新的认证状态
if (window.location.pathname === '/home' && !isAuthenticated) { if (window.location.pathname === '/home' && !isAuthenticatedRef.current) {
checkSession(); checkSession();
} }
}; };
@@ -91,7 +108,7 @@ export const AuthProvider = ({ children }) => {
window.addEventListener('popstate', handleRouteChange); window.addEventListener('popstate', handleRouteChange);
return () => window.removeEventListener('popstate', handleRouteChange); return () => window.removeEventListener('popstate', handleRouteChange);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAuthenticated]); }, []); // ✅ 空依赖数组,只注册一次事件监听器
// 更新本地用户的便捷方法 // 更新本地用户的便捷方法
const updateUser = (partial) => { const updateUser = (partial) => {