diff --git a/app/_layout.tsx b/app/_layout.tsx index 6c01cda..8bbc8c9 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -101,15 +101,18 @@ function SystemChrome() { } function SessionGate({ children }: { children: React.ReactNode }) { - const [ready, setReady] = useState(false); + const isAuthenticated = useAuthStore((s) => s.isAuthenticated); const fetchCurrentUser = useAuthStore((s) => s.fetchCurrentUser); const colors = useAppColors(); + const [verified, setVerified] = useState(false); useEffect(() => { - fetchCurrentUser().finally(() => setReady(true)); + fetchCurrentUser().finally(() => setVerified(true)); }, [fetchCurrentUser]); - if (!ready) { + // If persisted state shows authenticated, render immediately (background verify) + // If not authenticated and not yet verified, show loading + if (!isAuthenticated && !verified) { return ( ); } + // If verified and not authenticated, the Redirect in (app)/_layout.tsx handles it return <>{children}; } diff --git a/src/hooks/useChannels.ts b/src/hooks/useChannels.ts new file mode 100644 index 0000000..ee2bb57 --- /dev/null +++ b/src/hooks/useChannels.ts @@ -0,0 +1,14 @@ +import { useQuery } from '@tanstack/react-query'; +import { channelService, type ChannelItem } from '@/services/post/channelService'; + +export const channelKeys = { + all: ['channels'] as const, +}; + +export function useChannels() { + return useQuery({ + queryKey: channelKeys.all, + queryFn: () => channelService.list(), + staleTime: 10 * 60 * 1000, + }); +} \ No newline at end of file diff --git a/src/hooks/useUnreadCount.ts b/src/hooks/useUnreadCount.ts new file mode 100644 index 0000000..bb1ce0b --- /dev/null +++ b/src/hooks/useUnreadCount.ts @@ -0,0 +1,15 @@ +import { useQuery } from '@tanstack/react-query'; +import { messageManager } from '@/stores/message/MessageManager'; + +export const messageKeys = { + unread: ['messages', 'unread'] as const, +}; + +export function useUnreadCountQuery() { + return useQuery({ + queryKey: messageKeys.unread, + queryFn: () => messageManager.fetchUnreadCount(), + staleTime: 30 * 1000, + refetchInterval: 60 * 1000, + }); +} diff --git a/src/screens/auth/RegisterScreen.tsx b/src/screens/auth/RegisterScreen.tsx index 625cf81..d3a66dc 100644 --- a/src/screens/auth/RegisterScreen.tsx +++ b/src/screens/auth/RegisterScreen.tsx @@ -77,7 +77,7 @@ export const RegisterScreen: React.FC = () => { resetRegisterData, goToNextStep, goToPrevStep, - } = useRegisterStore(); + } = useRegisterStore.getState(); // 本地状态 const [loading, setLoading] = React.useState(false); diff --git a/src/screens/home/HomeScreen.tsx b/src/screens/home/HomeScreen.tsx index 96dfe86..d7551ae 100644 --- a/src/screens/home/HomeScreen.tsx +++ b/src/screens/home/HomeScreen.tsx @@ -29,6 +29,7 @@ import { Post } from '../../types'; import { useUserStore, useHomeTabBarVisibilityStore, useHomeTabPressStore } from '../../stores'; import { useCurrentUser, useIsAuthenticated, useIsVerified, useVerificationStore } from '../../stores/auth'; import { channelService, postService } from '../../services'; +import { useChannels } from '../../hooks/useChannels'; import { PostCard, SearchBar, ShareSheet } from '../../components/business'; import type { PostCardAction } from '../../components/business/PostCard'; import { Loading, EmptyState, Text, ImageGallery, ImageGridItem } from '../../components/common'; @@ -215,7 +216,7 @@ function createHomeStyles(colors: AppColors, responsivePadding: number) { export const HomeScreen: React.FC = () => { const router = useRouter(); const insets = useSafeAreaInsets(); - const { posts: storePosts } = useUserStore(); + const storePosts = useUserStore((s) => s.posts); const currentUser = useCurrentUser(); const isAuthenticated = useIsAuthenticated(); const isVerified = useIsVerified(); @@ -242,13 +243,23 @@ export const HomeScreen: React.FC = () => { const [sortIndex, setSortIndex] = useState(2); const [viewMode, setViewMode] = useState('list'); - const [latestCapsules, setLatestCapsules] = useState([{ id: '', name: '全部' }]); + const { data: channelList = [] } = useChannels(); + const latestCapsules = useMemo( + () => [{ id: '', name: '全部' }, ...channelList.map(item => ({ id: item.id, name: item.name }))], + [channelList] + ); const [activeCapsuleId, setActiveCapsuleId] = useState(''); - + // 图片查看器状态 const [showImageViewer, setShowImageViewer] = useState(false); const [postImages, setPostImages] = useState([]); const [selectedImageIndex, setSelectedImageIndex] = useState(0); + + const stableCloseImageViewer = useCallback(() => setShowImageViewer(false), []); + const stableGalleryImages = useMemo(() => postImages.map((img, i) => ({ + id: img.id || img.url || `img-${i}`, + url: img.url || img.uri || '' + })), [postImages]); // 搜索显示状态(用于内嵌搜索页面) const [showSearch, setShowSearch] = useState(false); @@ -587,18 +598,6 @@ export const HomeScreen: React.FC = () => { }); }, []); - useEffect(() => { - const loadChannels = async () => { - const list = await channelService.list(); - const capsules: LatestCapsule[] = [ - { id: '', name: '全部' }, - ...list.map(item => ({ id: item.id, name: item.name })), - ]; - setLatestCapsules(capsules); - }; - loadChannels(); - }, []); - // 跳转到搜索页(使用内嵌模式,不再依赖导航) const handleSearchPress = () => { setShowSearch(true); @@ -811,9 +810,10 @@ export const HomeScreen: React.FC = () => { const renderResponsiveGrid = () => { return ( { // 移动端和宽屏都使用单列 FlashList,宽屏下居中显示 return ( { {/* 图片查看器 */} ({ - id: img.id || img.url || String(Math.random()), - url: img.url || img.uri || '' - }))} + images={stableGalleryImages} initialIndex={selectedImageIndex} - onClose={() => setShowImageViewer(false)} + onClose={stableCloseImageViewer} enableSave /> diff --git a/src/screens/home/MarketView.tsx b/src/screens/home/MarketView.tsx index db476a7..aa9ff28 100644 --- a/src/screens/home/MarketView.tsx +++ b/src/screens/home/MarketView.tsx @@ -187,7 +187,8 @@ export function MarketView({ renderItem={renderItem} keyExtractor={keyExtractor} numColumns={numColumns} - key={`market_${numColumns}`} + key="market-list" + extraData={numColumns} contentContainerStyle={styles.listContent} showsVerticalScrollIndicator={false} refreshControl={ diff --git a/src/screens/home/PostDetailScreen.tsx b/src/screens/home/PostDetailScreen.tsx index e13b980..0d880b2 100644 --- a/src/screens/home/PostDetailScreen.tsx +++ b/src/screens/home/PostDetailScreen.tsx @@ -1084,6 +1084,12 @@ export const PostDetailScreen: React.FC = () => { })); }, [post?.images]); + // 为 ImageGallery 提供稳定的图片列表,避免 Math.random() 在每次渲染时生成新 ID + const stableGalleryImages = useMemo(() => allImages.map((img, i) => ({ + id: img.id || img.url || `img-${i}`, + url: img.url || img.uri || '' + })), [allImages]); + // 渲染帖子头部 - 小红书/微博风格 const renderPostHeader = useCallback(() => { if (!post) return null; @@ -1740,10 +1746,7 @@ export const PostDetailScreen: React.FC = () => { {/* 图片预览 ImageGallery */} ({ - id: img.id || img.url || String(Math.random()), - url: img.url || img.uri || '' - }))} + images={stableGalleryImages} initialIndex={selectedImageIndex} onClose={() => setShowImageModal(false)} enableSave @@ -1815,10 +1818,7 @@ export const PostDetailScreen: React.FC = () => { {/* 图片预览 ImageGallery */} ({ - id: img.id || img.url || String(Math.random()), - url: img.url || img.uri || '' - }))} + images={stableGalleryImages} initialIndex={selectedImageIndex} onClose={() => setShowImageModal(false)} enableSave diff --git a/src/screens/home/SearchScreen.tsx b/src/screens/home/SearchScreen.tsx index d97b225..8d57b50 100644 --- a/src/screens/home/SearchScreen.tsx +++ b/src/screens/home/SearchScreen.tsx @@ -44,7 +44,8 @@ export const SearchScreen: React.FC = ({ onBack }) => { const styles = useMemo(() => createSearchScreenStyles(colors), [colors]); const router = useRouter(); const insets = useSafeAreaInsets(); - const { searchHistory: history, addSearchHistory, clearSearchHistory } = useUserStore(); + const history = useUserStore((state) => state.searchHistory); + const { addSearchHistory, clearSearchHistory } = useUserStore.getState(); // 使用响应式 hook const { diff --git a/src/screens/message/ChatScreen.tsx b/src/screens/message/ChatScreen.tsx index ca0f92d..e432f1e 100644 --- a/src/screens/message/ChatScreen.tsx +++ b/src/screens/message/ChatScreen.tsx @@ -111,6 +111,11 @@ export const ChatScreen: React.FC = (props) => { const [chatImages, setChatImages] = useState([]); const [selectedImageIndex, setSelectedImageIndex] = useState(0); + const stableGalleryImages = useMemo(() => chatImages.map((img, i) => ({ + id: img.id || img.url || `img-${i}`, + url: img.url || img.uri || '' + })), [chatImages]); + // 图片点击处理函数 const handleImagePress = (images: ImageGridItem[], index: number) => { setChatImages(images); @@ -212,6 +217,28 @@ export const ChatScreen: React.FC = (props) => { jumpToLatestMessages, setBrowsingHistory, } = useChatScreen(props); + + const stableOnBack = useCallback(() => router.back(), [router]); + const stableOnFocusInput = useCallback(() => { + if (activePanel !== 'none' && activePanel !== 'mention') { + closePanel(); + } + }, [activePanel, closePanel]); + const stableFocusTextInput = useCallback(() => textInputRef.current?.focus(), []); + const stableOnLayout = useCallback((e: any) => { + scrollPositionRef.current.viewportHeight = e.nativeEvent.layout.height; + }, []); + const stableOnScrollBeginDrag = useCallback(() => { + isUserDraggingRef.current = true; + handleDismiss(); + }, [handleDismiss]); + const stableOnScrollEndDrag = useCallback(() => { + isUserDraggingRef.current = false; + }, []); + const stableOnMomentumScrollEnd = useCallback(() => { + isUserDraggingRef.current = false; + }, []); + const displayMessages = useMemo(() => [...messages].reverse(), [messages]); const longPressMenuMemberMap = useMemo(() => { @@ -460,7 +487,7 @@ export const ChatScreen: React.FC = (props) => { otherUser={otherUser} routeGroupName={effectiveGroupName ?? undefined} typingHint={typingHint} - onBack={props.onEmbeddedBack ? props.onEmbeddedBack : () => router.back()} + onBack={props.onEmbeddedBack || stableOnBack} onTitlePress={navigateToInfo} onMorePress={navigateToChatSettings} onGroupInfoPress={handleGroupInfoPress} @@ -470,9 +497,7 @@ export const ChatScreen: React.FC = (props) => { {/* 消息列表 */} { - scrollPositionRef.current.viewportHeight = e.nativeEvent.layout.height; - }} + onLayout={stableOnLayout} onTouchEnd={handleDismiss} > {loading ? ( @@ -493,16 +518,9 @@ export const ChatScreen: React.FC = (props) => { scrollEnabled={true} drawDistance={250} onScroll={handleMessageListScroll} - onScrollBeginDrag={() => { - isUserDraggingRef.current = true; - handleDismiss(); - }} - onScrollEndDrag={() => { - isUserDraggingRef.current = false; - }} - onMomentumScrollEnd={() => { - isUserDraggingRef.current = false; - }} + onScrollBeginDrag={stableOnScrollBeginDrag} + onScrollEndDrag={stableOnScrollEndDrag} + onMomentumScrollEnd={stableOnMomentumScrollEnd} scrollEventThrottle={16} onContentSizeChange={handleContentSizeChange} /> @@ -568,12 +586,7 @@ export const ChatScreen: React.FC = (props) => { pendingAttachments={pendingAttachments} onRemovePendingAttachment={removePendingAttachment} onCancelReply={handleCancelReply} - onFocus={() => { - // 输入框获得焦点时,关闭其他面板(但不要关闭键盘) - if (activePanel !== 'none' && activePanel !== 'mention') { - closePanel(); - } - }} + onFocus={stableOnFocusInput} currentUser={currentUser} otherUser={otherUser} getSenderInfo={getSenderInfo} @@ -587,7 +600,7 @@ export const ChatScreen: React.FC = (props) => { onInsertEmoji={handleInsertEmoji} onInsertSticker={handleSendSticker} onClose={closePanel} - onFocusInput={() => textInputRef.current?.focus()} + onFocusInput={stableFocusTextInput} /> )} @@ -657,10 +670,7 @@ export const ChatScreen: React.FC = (props) => { {/* 图片查看器 */} ({ - id: img.id || img.url || String(Math.random()), - url: img.url || img.uri || '' - }))} + images={stableGalleryImages} initialIndex={selectedImageIndex} onClose={handleCloseImageViewer} enableSave diff --git a/src/screens/message/MessageListScreen.tsx b/src/screens/message/MessageListScreen.tsx index 6bccd59..fbafd34 100644 --- a/src/screens/message/MessageListScreen.tsx +++ b/src/screens/message/MessageListScreen.tsx @@ -152,6 +152,11 @@ export const MessageListScreen: React.FC = () => { // 系统通知显示状态 - 用于在移动端显示通知页面 const [showNotifications, setShowNotifications] = useState(false); + // Stable callbacks to avoid inline arrows passed to child components + const stableCloseNotifications = useCallback(() => setShowNotifications(false), []); + const stableClearSelectedConversation = useCallback(() => setSelectedConversation(null), []); + const stableCloseScanner = useCallback(() => setScannerVisible(false), []); + // 系统消息会话对象(用于选中状态) const systemMessageConversation: ConversationResponse = useMemo(() => ({ id: SYSTEM_MESSAGE_CHANNEL_ID, @@ -197,14 +202,11 @@ export const MessageListScreen: React.FC = () => { return FLOATING_TAB_BAR_HEIGHT + insets.bottom + spacing.md; }, [isWideScreen, insets.bottom]); - // 【新架构】页面获得焦点时初始化MessageManager + // 初始化MessageManager(仅首次),焦点时轻量刷新会话列表 useEffect(() => { - if (isFocused) { - messageManager.initialize(); - } - }, [isFocused]); + messageManager.initialize(); + }, []); - // 【新架构】使用focus刷新hook,从ChatScreen返回时自动刷新未读数 useMessageListRefresh(); // 同步未读数到userStore(用于TabBar角标显示) @@ -840,7 +842,7 @@ export const MessageListScreen: React.FC = () => { {showNotifications ? ( // 显示系统通知页面 - setShowNotifications(false)} /> + ) : selectedConversation ? ( // 显示选中会话的聊天内容 { embeddedIsGroupChat={selectedConversation.type === 'group'} embeddedGroupId={selectedConversation.group ? String(selectedConversation.group.id) : undefined} embeddedGroupName={selectedConversation.group?.name} - onEmbeddedBack={() => setSelectedConversation(null)} + onEmbeddedBack={stableClearSelectedConversation} /> ) : ( // 默认占位符 @@ -871,7 +873,7 @@ export const MessageListScreen: React.FC = () => { {showNotifications ? ( // 显示系统通知页面,传入 onBack 回调 - setShowNotifications(false)} /> + ) : isSearchMode ? ( renderSearchMode() ) : isWideScreen ? ( @@ -882,7 +884,7 @@ export const MessageListScreen: React.FC = () => { renderConversationList() )} {renderActionMenu()} - setScannerVisible(false)} /> + ); }; diff --git a/src/screens/profile/FollowListScreen.tsx b/src/screens/profile/FollowListScreen.tsx index 72091ab..67be167 100644 --- a/src/screens/profile/FollowListScreen.tsx +++ b/src/screens/profile/FollowListScreen.tsx @@ -33,7 +33,7 @@ const FollowListScreen: React.FC = () => { const type = typeParam === 'followers' ? 'followers' : 'following'; const { currentUser } = useAuthStore(); - const { followUser, unfollowUser } = useUserStore(); + const { followUser, unfollowUser } = useUserStore.getState(); // 响应式布局 const { isWideScreen, isDesktop, width } = useResponsive(); diff --git a/src/screens/profile/useUserProfile.ts b/src/screens/profile/useUserProfile.ts index 8f431e7..bd889b6 100644 --- a/src/screens/profile/useUserProfile.ts +++ b/src/screens/profile/useUserProfile.ts @@ -72,7 +72,8 @@ export const useUserProfile = (options: UseUserProfileOptions): UseUserProfileRe const insets = useSafeAreaInsets(); const router = useRouter(); - const { followUser, unfollowUser, posts: storePosts } = useUserStore(); + const storePosts = useUserStore((state) => state.posts); + const { followUser, unfollowUser } = useUserStore.getState(); const currentUser = useCurrentUser(); // 状态 diff --git a/src/screens/trade/TradeDetailScreen.tsx b/src/screens/trade/TradeDetailScreen.tsx index 6181c46..ee95dcf 100644 --- a/src/screens/trade/TradeDetailScreen.tsx +++ b/src/screens/trade/TradeDetailScreen.tsx @@ -634,6 +634,11 @@ export function TradeDetailScreen({ tradeId }: TradeDetailScreenProps) { })); }, [images]); + const stableTradeGalleryImages = useMemo(() => tradeImages.map((img, i) => ({ + id: img.id || img.url || `trade-img-${i}`, + url: img.url || '', + })), [tradeImages]); + const handleImagePress = useCallback((allImages: any[], index: number) => { setSelectedImageIndex(index); setShowImageModal(true); @@ -965,10 +970,7 @@ export function TradeDetailScreen({ tradeId }: TradeDetailScreenProps) { {/* ─── 图片预览 ─── */} ({ - id: img.id || img.url || String(Math.random()), - url: img.url || '', - }))} + images={stableTradeGalleryImages} initialIndex={selectedImageIndex} onClose={() => setShowImageModal(false)} enableSave diff --git a/src/stores/auth/authStore.ts b/src/stores/auth/authStore.ts index b6391c3..85d0177 100644 --- a/src/stores/auth/authStore.ts +++ b/src/stores/auth/authStore.ts @@ -12,6 +12,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { create } from 'zustand'; +import { persist, createJSONStorage } from 'zustand/middleware'; import { User } from '../../types'; import { authService, resolveAuthApiError, LoginRequest, RegisterRequest } from '../../services'; import { wsService } from '@/services/core'; @@ -148,15 +149,17 @@ interface AuthState { fetchCurrentUser: () => Promise; } -export const useAuthStore = create((set) => ({ - isAuthenticated: false, - currentUser: null, - isLoading: false, - error: null, +export const useAuthStore = create()( + persist( + (set) => ({ + isAuthenticated: false, + currentUser: null, + isLoading: false, + error: null, - // ── 登录 ── - // 流程:API → 保存Token → 初始化DB → 持久化userId → 写缓存 → 设置状态 - login: async (credentials: LoginRequest) => { + // ── 登录 ── + // 流程:API → 保存Token → 初始化DB → 持久化userId → 写缓存 → 设置状态 + login: async (credentials: LoginRequest) => { set({ isLoading: true, error: null }); try { @@ -396,7 +399,17 @@ export const useAuthStore = create((set) => ({ setLoading: (loading: boolean) => set({ isLoading: loading }), setError: (error: string | null) => set({ error }), -})); +}), + { + name: 'auth-session', + storage: createJSONStorage(() => AsyncStorage), + partialize: (state) => ({ + isAuthenticated: state.isAuthenticated, + currentUser: state.currentUser, + }), + } + ) +); // 导出 selector hooks 以优化性能 export const useCurrentUser = () => useAuthStore((state) => state.currentUser); diff --git a/src/stores/message/baseHooks.ts b/src/stores/message/baseHooks.ts index 972aa41..4ac1eba 100644 --- a/src/stores/message/baseHooks.ts +++ b/src/stores/message/baseHooks.ts @@ -97,13 +97,14 @@ export function useMessages( loadMoreMessages: (conversationId: string, beforeSeq: number, limit?: number) => Promise ): UseMessagesReturn { const normalizedConversationId = normalizeConversationId(conversationId); - const store = useMessageStore(); - + // 使用 useShallow 避免每次返回新数组引用导致的无限循环 const messages = useMessageStore( useShallow(state => state.messagesMap.get(normalizedConversationId) || []) ); const isLoadingMessages = useMessageStore(state => state.loadingMessagesSet.has(normalizedConversationId)); + const getActiveConversation = useMessageStore.getState().getActiveConversation; + const setCurrentConversation = useMessageStore.getState().setCurrentConversation; const [hasMore, setHasMore] = useState(true); useEffect(() => { @@ -114,8 +115,8 @@ export function useMessages( return () => { // 清理活动会话 - if (store.getActiveConversation() === normalizedConversationId) { - store.setCurrentConversation(null); + if (getActiveConversation() === normalizedConversationId) { + setCurrentConversation(null); } }; }, [normalizedConversationId]); @@ -428,11 +429,10 @@ interface UseGroupMutedReturn { export function useGroupMuted(groupId: string): UseGroupMutedReturn { // 使用 Zustand selector 直接订阅禁言状态 const isMuted = useMessageStore(state => state.mutedStatusMap.get(groupId) || false); - const store = useMessageStore(); const setMutedStatus = useCallback((muted: boolean) => { - store.setMutedStatus(groupId, muted); - }, [groupId, store]); + useMessageStore.getState().setMutedStatus(groupId, muted); + }, [groupId]); return { isMuted, diff --git a/src/stores/message/hooks.ts b/src/stores/message/hooks.ts index 6e0c0e4..fe65eb7 100644 --- a/src/stores/message/hooks.ts +++ b/src/stores/message/hooks.ts @@ -16,6 +16,7 @@ import { useShallow } from 'zustand/react/shallow'; import { ConversationResponse, MessageResponse, MessageSegment } from '../../types/dto'; import { messageManager } from './MessageManager'; import { useMessageStore } from './store'; +import { useUnreadCountQuery } from '../../hooks/useUnreadCount'; // ==================== useConversations - 获取会话列表 ==================== @@ -43,16 +44,19 @@ export function useConversations(): UseConversationsReturn { console.error('[useConversations] 初始化失败:', error); }); - // 冷启动兜底:延迟做一次强制刷新,避免首次因时序问题拿到空列表 - const coldStartSyncTimer = setTimeout(() => { - messageManager.refreshConversations(true, 'hooks-initial-refresh').catch(error => { - console.error('[useConversations] 冷启动强制刷新失败:', error); - }); - }, 1200); + // 仅在首次初始化时做一次冷启动兜底刷新,避免每次挂载都触发 + const store = useMessageStore.getState(); + if (!store.isInitialized) { + const coldStartSyncTimer = setTimeout(() => { + messageManager.refreshConversations(true, 'hooks-initial-refresh').catch(error => { + console.error('[useConversations] 冷启动强制刷新失败:', error); + }); + }, 1200); - return () => { - clearTimeout(coldStartSyncTimer); - }; + return () => { + clearTimeout(coldStartSyncTimer); + }; + } }, []); // 监听 hasMore 变化 @@ -249,15 +253,11 @@ interface UseUnreadCountReturn { * 使用 Zustand selector 直接订阅未读数变化 */ export function useUnreadCount(): UseUnreadCountReturn { - // 使用 Zustand selector 直接订阅未读数 + // Use React Query for fetching (caching + auto-refetch), Zustand selectors for reading + useUnreadCountQuery(); const totalUnreadCount = useMessageStore(state => state.totalUnreadCount); const systemUnreadCount = useMessageStore(state => state.systemUnreadCount); - useEffect(() => { - // 初始化时获取最新未读数 - messageManager.fetchUnreadCount(); - }, []); - return { totalUnreadCount, systemUnreadCount, @@ -271,14 +271,11 @@ export function useUnreadCount(): UseUnreadCountReturn { * 使用 Zustand selector 直接订阅 */ export function useTotalUnreadCount(): number { + // Use React Query for fetching, Zustand selectors for reading + useUnreadCountQuery(); const totalUnreadCount = useMessageStore(state => state.totalUnreadCount); const systemUnreadCount = useMessageStore(state => state.systemUnreadCount); - useEffect(() => { - // 初始化时获取最新未读数 - messageManager.fetchUnreadCount(); - }, []); - return totalUnreadCount + systemUnreadCount; } diff --git a/src/stores/settings/themeStore.ts b/src/stores/settings/themeStore.ts index 5173b7c..924f856 100644 --- a/src/stores/settings/themeStore.ts +++ b/src/stores/settings/themeStore.ts @@ -34,6 +34,24 @@ function buildState( }; } +let _cachedKey = ''; +let _cachedState: ReturnType; + +function buildStateCached( + preference: ThemePreference, + systemScheme: ResolvedScheme +): { + resolvedScheme: ResolvedScheme; + colors: AppColors; + paperTheme: MD3Theme; +} { + const key = `${preference}:${systemScheme}`; + if (_cachedKey === key && _cachedState) return _cachedState; + _cachedKey = key; + _cachedState = buildState(preference, systemScheme); + return _cachedState; +} + type ThemeState = { preference: ThemePreference; systemScheme: ResolvedScheme; @@ -58,14 +76,14 @@ export const useThemeStore = create((set, get) => ({ preference: 'system', systemScheme: initialSystemScheme, hydrated: false, - ...buildState('system', initialSystemScheme), + ...buildStateCached('system', initialSystemScheme), setSystemScheme: (systemScheme) => { const { preference, systemScheme: cur } = get(); if (cur === systemScheme) return; set({ systemScheme, - ...buildState(preference, systemScheme), + ...buildStateCached(preference, systemScheme), }); }, @@ -78,7 +96,7 @@ export const useThemeStore = create((set, get) => ({ const { systemScheme } = get(); set({ preference, - ...buildState(preference, systemScheme), + ...buildStateCached(preference, systemScheme), }); }, @@ -97,7 +115,7 @@ export const useThemeStore = create((set, get) => ({ preference, hydrated: true, systemScheme, - ...buildState(preference, systemScheme), + ...buildStateCached(preference, systemScheme), }); }, })); diff --git a/src/stores/userStore.ts b/src/stores/userStore.ts index adb4228..bfefe94 100644 --- a/src/stores/userStore.ts +++ b/src/stores/userStore.ts @@ -5,6 +5,7 @@ */ import { create } from 'zustand'; +import { useShallow } from 'zustand/react/shallow'; import { User, Post, Notification, NotificationBadge } from '../types'; import { PaginatedData } from '@/services/core'; import { @@ -320,7 +321,7 @@ export const useUserStore = create((set, get) => { // 导出selector hooks以优化性能 export const usePosts = () => useUserStore((state) => state.posts); export const useNotifications = () => useUserStore((state) => state.notifications); -export const useNotificationBadge = () => useUserStore((state) => state.notificationBadge); +export const useNotificationBadge = () => useUserStore(useShallow((state) => state.notificationBadge)); export const useMessageUnreadCount = () => useUserStore((state) => state.messageUnreadCount); export const useSearchHistory = () => useUserStore((state) => state.searchHistory); export const useIsLoadingPosts = () => useUserStore((state) => state.isLoadingPosts);