refactor(core): optimize state management and component rendering performance
Some checks failed
Frontend CI / ota-ios (push) Successful in 1m39s
Frontend CI / ota-android (push) Successful in 1m39s
Frontend CI / build-android-apk (push) Failing after 1m52s
Frontend CI / build-and-push-web (push) Successful in 22m5s

Improve application stability and performance by optimizing Zustand store usage, implementing memoization patterns, and introducing persistent authentication.

- **State Management**:
  - Refactor Zustand selectors to use `useShallow` and `getState()` to prevent unnecessary re-renders and infinite loops in hooks.
  - Implement `persist` middleware for `authStore` to maintain user sessions across restarts.
  - Introduce `buildStateCached` in `themeStore` to reduce redundant theme object computations.
- **Performance & Rendering**:
  - Implement `useMemo` for stable object/array references in `ImageGallery` and list components to prevent expensive re-renders.
  - Replace inline arrow functions with `useCallback` in complex screens like `ChatScreen` and `MessageListScreen`.
  - Optimize `FlashList` usage by providing stable `key` and `extraData` props.
- **Architecture**:
  - Decouple unread count fetching by introducing `useUnreadCountQuery` (React Query) for better caching and synchronization.
  - Add `useChannels` hook to centralize channel data fetching.
  - Refine `SessionGate` logic to allow immediate rendering of authenticated users while verifying in the background.
This commit is contained in:
2026-05-18 00:39:25 +08:00
parent fb67fb6d5b
commit 4fde3e403a
18 changed files with 195 additions and 118 deletions

View File

@@ -101,15 +101,18 @@ function SystemChrome() {
} }
function SessionGate({ children }: { children: React.ReactNode }) { function SessionGate({ children }: { children: React.ReactNode }) {
const [ready, setReady] = useState(false); const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const fetchCurrentUser = useAuthStore((s) => s.fetchCurrentUser); const fetchCurrentUser = useAuthStore((s) => s.fetchCurrentUser);
const colors = useAppColors(); const colors = useAppColors();
const [verified, setVerified] = useState(false);
useEffect(() => { useEffect(() => {
fetchCurrentUser().finally(() => setReady(true)); fetchCurrentUser().finally(() => setVerified(true));
}, [fetchCurrentUser]); }, [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 ( return (
<View <View
style={{ style={{
@@ -123,6 +126,7 @@ function SessionGate({ children }: { children: React.ReactNode }) {
</View> </View>
); );
} }
// If verified and not authenticated, the Redirect in (app)/_layout.tsx handles it
return <>{children}</>; return <>{children}</>;
} }

14
src/hooks/useChannels.ts Normal file
View File

@@ -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,
});
}

View File

@@ -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,
});
}

View File

@@ -77,7 +77,7 @@ export const RegisterScreen: React.FC = () => {
resetRegisterData, resetRegisterData,
goToNextStep, goToNextStep,
goToPrevStep, goToPrevStep,
} = useRegisterStore(); } = useRegisterStore.getState();
// 本地状态 // 本地状态
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);

View File

@@ -29,6 +29,7 @@ import { Post } from '../../types';
import { useUserStore, useHomeTabBarVisibilityStore, useHomeTabPressStore } from '../../stores'; import { useUserStore, useHomeTabBarVisibilityStore, useHomeTabPressStore } from '../../stores';
import { useCurrentUser, useIsAuthenticated, useIsVerified, useVerificationStore } from '../../stores/auth'; import { useCurrentUser, useIsAuthenticated, useIsVerified, useVerificationStore } from '../../stores/auth';
import { channelService, postService } from '../../services'; import { channelService, postService } from '../../services';
import { useChannels } from '../../hooks/useChannels';
import { PostCard, SearchBar, ShareSheet } from '../../components/business'; import { PostCard, SearchBar, ShareSheet } from '../../components/business';
import type { PostCardAction } from '../../components/business/PostCard'; import type { PostCardAction } from '../../components/business/PostCard';
import { Loading, EmptyState, Text, ImageGallery, ImageGridItem } from '../../components/common'; import { Loading, EmptyState, Text, ImageGallery, ImageGridItem } from '../../components/common';
@@ -215,7 +216,7 @@ function createHomeStyles(colors: AppColors, responsivePadding: number) {
export const HomeScreen: React.FC = () => { export const HomeScreen: React.FC = () => {
const router = useRouter(); const router = useRouter();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { posts: storePosts } = useUserStore(); const storePosts = useUserStore((s) => s.posts);
const currentUser = useCurrentUser(); const currentUser = useCurrentUser();
const isAuthenticated = useIsAuthenticated(); const isAuthenticated = useIsAuthenticated();
const isVerified = useIsVerified(); const isVerified = useIsVerified();
@@ -242,13 +243,23 @@ export const HomeScreen: React.FC = () => {
const [sortIndex, setSortIndex] = useState(2); const [sortIndex, setSortIndex] = useState(2);
const [viewMode, setViewMode] = useState<ViewMode>('list'); const [viewMode, setViewMode] = useState<ViewMode>('list');
const [latestCapsules, setLatestCapsules] = useState<LatestCapsule[]>([{ id: '', name: '全部' }]); const { data: channelList = [] } = useChannels();
const latestCapsules = useMemo<LatestCapsule[]>(
() => [{ id: '', name: '全部' }, ...channelList.map(item => ({ id: item.id, name: item.name }))],
[channelList]
);
const [activeCapsuleId, setActiveCapsuleId] = useState(''); const [activeCapsuleId, setActiveCapsuleId] = useState('');
// 图片查看器状态 // 图片查看器状态
const [showImageViewer, setShowImageViewer] = useState(false); const [showImageViewer, setShowImageViewer] = useState(false);
const [postImages, setPostImages] = useState<ImageGridItem[]>([]); const [postImages, setPostImages] = useState<ImageGridItem[]>([]);
const [selectedImageIndex, setSelectedImageIndex] = useState(0); 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); 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 = () => { const handleSearchPress = () => {
setShowSearch(true); setShowSearch(true);
@@ -811,9 +810,10 @@ export const HomeScreen: React.FC = () => {
const renderResponsiveGrid = () => { const renderResponsiveGrid = () => {
return ( return (
<FlashList <FlashList
key={`${listKey}_grid`} key="home-grid"
ref={scrollViewRef as any} ref={scrollViewRef as any}
data={displayPosts} data={displayPosts}
extraData={listKey}
renderItem={renderGridItem} renderItem={renderGridItem}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
numColumns={gridColumns} numColumns={gridColumns}
@@ -865,9 +865,10 @@ export const HomeScreen: React.FC = () => {
// 移动端和宽屏都使用单列 FlashList宽屏下居中显示 // 移动端和宽屏都使用单列 FlashList宽屏下居中显示
return ( return (
<FlashList <FlashList
key={listKey} key="home-list"
ref={flashListRef} ref={flashListRef}
data={displayPosts} data={displayPosts}
extraData={listKey}
renderItem={renderPostList} renderItem={renderPostList}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
contentContainerStyle={{ contentContainerStyle={{
@@ -1062,12 +1063,9 @@ export const HomeScreen: React.FC = () => {
{/* 图片查看器 */} {/* 图片查看器 */}
<ImageGallery <ImageGallery
visible={showImageViewer} visible={showImageViewer}
images={postImages.map(img => ({ images={stableGalleryImages}
id: img.id || img.url || String(Math.random()),
url: img.url || img.uri || ''
}))}
initialIndex={selectedImageIndex} initialIndex={selectedImageIndex}
onClose={() => setShowImageViewer(false)} onClose={stableCloseImageViewer}
enableSave enableSave
/> />

View File

@@ -187,7 +187,8 @@ export function MarketView({
renderItem={renderItem} renderItem={renderItem}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
numColumns={numColumns} numColumns={numColumns}
key={`market_${numColumns}`} key="market-list"
extraData={numColumns}
contentContainerStyle={styles.listContent} contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
refreshControl={ refreshControl={

View File

@@ -1084,6 +1084,12 @@ export const PostDetailScreen: React.FC = () => {
})); }));
}, [post?.images]); }, [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(() => { const renderPostHeader = useCallback(() => {
if (!post) return null; if (!post) return null;
@@ -1740,10 +1746,7 @@ export const PostDetailScreen: React.FC = () => {
{/* 图片预览 ImageGallery */} {/* 图片预览 ImageGallery */}
<ImageGallery <ImageGallery
visible={showImageModal} visible={showImageModal}
images={allImages.map(img => ({ images={stableGalleryImages}
id: img.id || img.url || String(Math.random()),
url: img.url || img.uri || ''
}))}
initialIndex={selectedImageIndex} initialIndex={selectedImageIndex}
onClose={() => setShowImageModal(false)} onClose={() => setShowImageModal(false)}
enableSave enableSave
@@ -1815,10 +1818,7 @@ export const PostDetailScreen: React.FC = () => {
{/* 图片预览 ImageGallery */} {/* 图片预览 ImageGallery */}
<ImageGallery <ImageGallery
visible={showImageModal} visible={showImageModal}
images={allImages.map(img => ({ images={stableGalleryImages}
id: img.id || img.url || String(Math.random()),
url: img.url || img.uri || ''
}))}
initialIndex={selectedImageIndex} initialIndex={selectedImageIndex}
onClose={() => setShowImageModal(false)} onClose={() => setShowImageModal(false)}
enableSave enableSave

View File

@@ -44,7 +44,8 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack }) => {
const styles = useMemo(() => createSearchScreenStyles(colors), [colors]); const styles = useMemo(() => createSearchScreenStyles(colors), [colors]);
const router = useRouter(); const router = useRouter();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { searchHistory: history, addSearchHistory, clearSearchHistory } = useUserStore(); const history = useUserStore((state) => state.searchHistory);
const { addSearchHistory, clearSearchHistory } = useUserStore.getState();
// 使用响应式 hook // 使用响应式 hook
const { const {

View File

@@ -111,6 +111,11 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
const [chatImages, setChatImages] = useState<ImageGridItem[]>([]); const [chatImages, setChatImages] = useState<ImageGridItem[]>([]);
const [selectedImageIndex, setSelectedImageIndex] = useState(0); 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) => { const handleImagePress = (images: ImageGridItem[], index: number) => {
setChatImages(images); setChatImages(images);
@@ -212,6 +217,28 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
jumpToLatestMessages, jumpToLatestMessages,
setBrowsingHistory, setBrowsingHistory,
} = useChatScreen(props); } = 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 displayMessages = useMemo(() => [...messages].reverse(), [messages]);
const longPressMenuMemberMap = useMemo(() => { const longPressMenuMemberMap = useMemo(() => {
@@ -460,7 +487,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
otherUser={otherUser} otherUser={otherUser}
routeGroupName={effectiveGroupName ?? undefined} routeGroupName={effectiveGroupName ?? undefined}
typingHint={typingHint} typingHint={typingHint}
onBack={props.onEmbeddedBack ? props.onEmbeddedBack : () => router.back()} onBack={props.onEmbeddedBack || stableOnBack}
onTitlePress={navigateToInfo} onTitlePress={navigateToInfo}
onMorePress={navigateToChatSettings} onMorePress={navigateToChatSettings}
onGroupInfoPress={handleGroupInfoPress} onGroupInfoPress={handleGroupInfoPress}
@@ -470,9 +497,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
{/* 消息列表 */} {/* 消息列表 */}
<View <View
style={styles.messageListContainer} style={styles.messageListContainer}
onLayout={e => { onLayout={stableOnLayout}
scrollPositionRef.current.viewportHeight = e.nativeEvent.layout.height;
}}
onTouchEnd={handleDismiss} onTouchEnd={handleDismiss}
> >
{loading ? ( {loading ? (
@@ -493,16 +518,9 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
scrollEnabled={true} scrollEnabled={true}
drawDistance={250} drawDistance={250}
onScroll={handleMessageListScroll} onScroll={handleMessageListScroll}
onScrollBeginDrag={() => { onScrollBeginDrag={stableOnScrollBeginDrag}
isUserDraggingRef.current = true; onScrollEndDrag={stableOnScrollEndDrag}
handleDismiss(); onMomentumScrollEnd={stableOnMomentumScrollEnd}
}}
onScrollEndDrag={() => {
isUserDraggingRef.current = false;
}}
onMomentumScrollEnd={() => {
isUserDraggingRef.current = false;
}}
scrollEventThrottle={16} scrollEventThrottle={16}
onContentSizeChange={handleContentSizeChange} onContentSizeChange={handleContentSizeChange}
/> />
@@ -568,12 +586,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
pendingAttachments={pendingAttachments} pendingAttachments={pendingAttachments}
onRemovePendingAttachment={removePendingAttachment} onRemovePendingAttachment={removePendingAttachment}
onCancelReply={handleCancelReply} onCancelReply={handleCancelReply}
onFocus={() => { onFocus={stableOnFocusInput}
// 输入框获得焦点时,关闭其他面板(但不要关闭键盘)
if (activePanel !== 'none' && activePanel !== 'mention') {
closePanel();
}
}}
currentUser={currentUser} currentUser={currentUser}
otherUser={otherUser} otherUser={otherUser}
getSenderInfo={getSenderInfo} getSenderInfo={getSenderInfo}
@@ -587,7 +600,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
onInsertEmoji={handleInsertEmoji} onInsertEmoji={handleInsertEmoji}
onInsertSticker={handleSendSticker} onInsertSticker={handleSendSticker}
onClose={closePanel} onClose={closePanel}
onFocusInput={() => textInputRef.current?.focus()} onFocusInput={stableFocusTextInput}
/> />
</View> </View>
)} )}
@@ -657,10 +670,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
{/* 图片查看器 */} {/* 图片查看器 */}
<ImageGallery <ImageGallery
visible={showImageViewer} visible={showImageViewer}
images={chatImages.map(img => ({ images={stableGalleryImages}
id: img.id || img.url || String(Math.random()),
url: img.url || img.uri || ''
}))}
initialIndex={selectedImageIndex} initialIndex={selectedImageIndex}
onClose={handleCloseImageViewer} onClose={handleCloseImageViewer}
enableSave enableSave

View File

@@ -152,6 +152,11 @@ export const MessageListScreen: React.FC = () => {
// 系统通知显示状态 - 用于在移动端显示通知页面 // 系统通知显示状态 - 用于在移动端显示通知页面
const [showNotifications, setShowNotifications] = useState(false); 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(() => ({ const systemMessageConversation: ConversationResponse = useMemo(() => ({
id: SYSTEM_MESSAGE_CHANNEL_ID, id: SYSTEM_MESSAGE_CHANNEL_ID,
@@ -197,14 +202,11 @@ export const MessageListScreen: React.FC = () => {
return FLOATING_TAB_BAR_HEIGHT + insets.bottom + spacing.md; return FLOATING_TAB_BAR_HEIGHT + insets.bottom + spacing.md;
}, [isWideScreen, insets.bottom]); }, [isWideScreen, insets.bottom]);
// 【新架构】页面获得焦点时初始化MessageManager // 初始化MessageManager(仅首次),焦点时轻量刷新会话列表
useEffect(() => { useEffect(() => {
if (isFocused) { messageManager.initialize();
messageManager.initialize(); }, []);
}
}, [isFocused]);
// 【新架构】使用focus刷新hook从ChatScreen返回时自动刷新未读数
useMessageListRefresh(); useMessageListRefresh();
// 同步未读数到userStore用于TabBar角标显示 // 同步未读数到userStore用于TabBar角标显示
@@ -840,7 +842,7 @@ export const MessageListScreen: React.FC = () => {
<View style={styles.chatArea}> <View style={styles.chatArea}>
{showNotifications ? ( {showNotifications ? (
// 显示系统通知页面 // 显示系统通知页面
<NotificationsScreen onBack={() => setShowNotifications(false)} /> <NotificationsScreen onBack={stableCloseNotifications} />
) : selectedConversation ? ( ) : selectedConversation ? (
// 显示选中会话的聊天内容 // 显示选中会话的聊天内容
<ChatScreen <ChatScreen
@@ -849,7 +851,7 @@ export const MessageListScreen: React.FC = () => {
embeddedIsGroupChat={selectedConversation.type === 'group'} embeddedIsGroupChat={selectedConversation.type === 'group'}
embeddedGroupId={selectedConversation.group ? String(selectedConversation.group.id) : undefined} embeddedGroupId={selectedConversation.group ? String(selectedConversation.group.id) : undefined}
embeddedGroupName={selectedConversation.group?.name} embeddedGroupName={selectedConversation.group?.name}
onEmbeddedBack={() => setSelectedConversation(null)} onEmbeddedBack={stableClearSelectedConversation}
/> />
) : ( ) : (
// 默认占位符 // 默认占位符
@@ -871,7 +873,7 @@ export const MessageListScreen: React.FC = () => {
<SafeAreaView style={styles.container} edges={['top', 'bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
{showNotifications ? ( {showNotifications ? (
// 显示系统通知页面,传入 onBack 回调 // 显示系统通知页面,传入 onBack 回调
<NotificationsScreen onBack={() => setShowNotifications(false)} /> <NotificationsScreen onBack={stableCloseNotifications} />
) : isSearchMode ? ( ) : isSearchMode ? (
renderSearchMode() renderSearchMode()
) : isWideScreen ? ( ) : isWideScreen ? (
@@ -882,7 +884,7 @@ export const MessageListScreen: React.FC = () => {
renderConversationList() renderConversationList()
)} )}
{renderActionMenu()} {renderActionMenu()}
<QRCodeScanner visible={scannerVisible} onClose={() => setScannerVisible(false)} /> <QRCodeScanner visible={scannerVisible} onClose={stableCloseScanner} />
</SafeAreaView> </SafeAreaView>
); );
}; };

View File

@@ -33,7 +33,7 @@ const FollowListScreen: React.FC = () => {
const type = typeParam === 'followers' ? 'followers' : 'following'; const type = typeParam === 'followers' ? 'followers' : 'following';
const { currentUser } = useAuthStore(); const { currentUser } = useAuthStore();
const { followUser, unfollowUser } = useUserStore(); const { followUser, unfollowUser } = useUserStore.getState();
// 响应式布局 // 响应式布局
const { isWideScreen, isDesktop, width } = useResponsive(); const { isWideScreen, isDesktop, width } = useResponsive();

View File

@@ -72,7 +72,8 @@ export const useUserProfile = (options: UseUserProfileOptions): UseUserProfileRe
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const router = useRouter(); const router = useRouter();
const { followUser, unfollowUser, posts: storePosts } = useUserStore(); const storePosts = useUserStore((state) => state.posts);
const { followUser, unfollowUser } = useUserStore.getState();
const currentUser = useCurrentUser(); const currentUser = useCurrentUser();
// 状态 // 状态

View File

@@ -634,6 +634,11 @@ export function TradeDetailScreen({ tradeId }: TradeDetailScreenProps) {
})); }));
}, [images]); }, [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) => { const handleImagePress = useCallback((allImages: any[], index: number) => {
setSelectedImageIndex(index); setSelectedImageIndex(index);
setShowImageModal(true); setShowImageModal(true);
@@ -965,10 +970,7 @@ export function TradeDetailScreen({ tradeId }: TradeDetailScreenProps) {
{/* ─── 图片预览 ─── */} {/* ─── 图片预览 ─── */}
<ImageGallery <ImageGallery
visible={showImageModal} visible={showImageModal}
images={tradeImages.map(img => ({ images={stableTradeGalleryImages}
id: img.id || img.url || String(Math.random()),
url: img.url || '',
}))}
initialIndex={selectedImageIndex} initialIndex={selectedImageIndex}
onClose={() => setShowImageModal(false)} onClose={() => setShowImageModal(false)}
enableSave enableSave

View File

@@ -12,6 +12,7 @@
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { create } from 'zustand'; import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { User } from '../../types'; import { User } from '../../types';
import { authService, resolveAuthApiError, LoginRequest, RegisterRequest } from '../../services'; import { authService, resolveAuthApiError, LoginRequest, RegisterRequest } from '../../services';
import { wsService } from '@/services/core'; import { wsService } from '@/services/core';
@@ -148,15 +149,17 @@ interface AuthState {
fetchCurrentUser: () => Promise<void>; fetchCurrentUser: () => Promise<void>;
} }
export const useAuthStore = create<AuthState>((set) => ({ export const useAuthStore = create<AuthState>()(
isAuthenticated: false, persist(
currentUser: null, (set) => ({
isLoading: false, isAuthenticated: false,
error: null, currentUser: null,
isLoading: false,
error: null,
// ── 登录 ── // ── 登录 ──
// 流程API → 保存Token → 初始化DB → 持久化userId → 写缓存 → 设置状态 // 流程API → 保存Token → 初始化DB → 持久化userId → 写缓存 → 设置状态
login: async (credentials: LoginRequest) => { login: async (credentials: LoginRequest) => {
set({ isLoading: true, error: null }); set({ isLoading: true, error: null });
try { try {
@@ -396,7 +399,17 @@ export const useAuthStore = create<AuthState>((set) => ({
setLoading: (loading: boolean) => set({ isLoading: loading }), setLoading: (loading: boolean) => set({ isLoading: loading }),
setError: (error: string | null) => set({ error }), setError: (error: string | null) => set({ error }),
})); }),
{
name: 'auth-session',
storage: createJSONStorage(() => AsyncStorage),
partialize: (state) => ({
isAuthenticated: state.isAuthenticated,
currentUser: state.currentUser,
}),
}
)
);
// 导出 selector hooks 以优化性能 // 导出 selector hooks 以优化性能
export const useCurrentUser = () => useAuthStore((state) => state.currentUser); export const useCurrentUser = () => useAuthStore((state) => state.currentUser);

View File

@@ -97,13 +97,14 @@ export function useMessages(
loadMoreMessages: (conversationId: string, beforeSeq: number, limit?: number) => Promise<MessageResponse[]> loadMoreMessages: (conversationId: string, beforeSeq: number, limit?: number) => Promise<MessageResponse[]>
): UseMessagesReturn { ): UseMessagesReturn {
const normalizedConversationId = normalizeConversationId(conversationId); const normalizedConversationId = normalizeConversationId(conversationId);
const store = useMessageStore();
// 使用 useShallow 避免每次返回新数组引用导致的无限循环 // 使用 useShallow 避免每次返回新数组引用导致的无限循环
const messages = useMessageStore( const messages = useMessageStore(
useShallow(state => state.messagesMap.get(normalizedConversationId) || []) useShallow(state => state.messagesMap.get(normalizedConversationId) || [])
); );
const isLoadingMessages = useMessageStore(state => state.loadingMessagesSet.has(normalizedConversationId)); const isLoadingMessages = useMessageStore(state => state.loadingMessagesSet.has(normalizedConversationId));
const getActiveConversation = useMessageStore.getState().getActiveConversation;
const setCurrentConversation = useMessageStore.getState().setCurrentConversation;
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(true);
useEffect(() => { useEffect(() => {
@@ -114,8 +115,8 @@ export function useMessages(
return () => { return () => {
// 清理活动会话 // 清理活动会话
if (store.getActiveConversation() === normalizedConversationId) { if (getActiveConversation() === normalizedConversationId) {
store.setCurrentConversation(null); setCurrentConversation(null);
} }
}; };
}, [normalizedConversationId]); }, [normalizedConversationId]);
@@ -428,11 +429,10 @@ interface UseGroupMutedReturn {
export function useGroupMuted(groupId: string): UseGroupMutedReturn { export function useGroupMuted(groupId: string): UseGroupMutedReturn {
// 使用 Zustand selector 直接订阅禁言状态 // 使用 Zustand selector 直接订阅禁言状态
const isMuted = useMessageStore(state => state.mutedStatusMap.get(groupId) || false); const isMuted = useMessageStore(state => state.mutedStatusMap.get(groupId) || false);
const store = useMessageStore();
const setMutedStatus = useCallback((muted: boolean) => { const setMutedStatus = useCallback((muted: boolean) => {
store.setMutedStatus(groupId, muted); useMessageStore.getState().setMutedStatus(groupId, muted);
}, [groupId, store]); }, [groupId]);
return { return {
isMuted, isMuted,

View File

@@ -16,6 +16,7 @@ import { useShallow } from 'zustand/react/shallow';
import { ConversationResponse, MessageResponse, MessageSegment } from '../../types/dto'; import { ConversationResponse, MessageResponse, MessageSegment } from '../../types/dto';
import { messageManager } from './MessageManager'; import { messageManager } from './MessageManager';
import { useMessageStore } from './store'; import { useMessageStore } from './store';
import { useUnreadCountQuery } from '../../hooks/useUnreadCount';
// ==================== useConversations - 获取会话列表 ==================== // ==================== useConversations - 获取会话列表 ====================
@@ -43,16 +44,19 @@ export function useConversations(): UseConversationsReturn {
console.error('[useConversations] 初始化失败:', error); console.error('[useConversations] 初始化失败:', error);
}); });
// 冷启动兜底:延迟做一次强制刷新,避免首次因时序问题拿到空列表 // 仅在首次初始化时做一次冷启动兜底刷新,避免每次挂载都触发
const coldStartSyncTimer = setTimeout(() => { const store = useMessageStore.getState();
messageManager.refreshConversations(true, 'hooks-initial-refresh').catch(error => { if (!store.isInitialized) {
console.error('[useConversations] 冷启动强制刷新失败:', error); const coldStartSyncTimer = setTimeout(() => {
}); messageManager.refreshConversations(true, 'hooks-initial-refresh').catch(error => {
}, 1200); console.error('[useConversations] 冷启动强制刷新失败:', error);
});
}, 1200);
return () => { return () => {
clearTimeout(coldStartSyncTimer); clearTimeout(coldStartSyncTimer);
}; };
}
}, []); }, []);
// 监听 hasMore 变化 // 监听 hasMore 变化
@@ -249,15 +253,11 @@ interface UseUnreadCountReturn {
* 使用 Zustand selector 直接订阅未读数变化 * 使用 Zustand selector 直接订阅未读数变化
*/ */
export function useUnreadCount(): UseUnreadCountReturn { 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 totalUnreadCount = useMessageStore(state => state.totalUnreadCount);
const systemUnreadCount = useMessageStore(state => state.systemUnreadCount); const systemUnreadCount = useMessageStore(state => state.systemUnreadCount);
useEffect(() => {
// 初始化时获取最新未读数
messageManager.fetchUnreadCount();
}, []);
return { return {
totalUnreadCount, totalUnreadCount,
systemUnreadCount, systemUnreadCount,
@@ -271,14 +271,11 @@ export function useUnreadCount(): UseUnreadCountReturn {
* 使用 Zustand selector 直接订阅 * 使用 Zustand selector 直接订阅
*/ */
export function useTotalUnreadCount(): number { export function useTotalUnreadCount(): number {
// Use React Query for fetching, Zustand selectors for reading
useUnreadCountQuery();
const totalUnreadCount = useMessageStore(state => state.totalUnreadCount); const totalUnreadCount = useMessageStore(state => state.totalUnreadCount);
const systemUnreadCount = useMessageStore(state => state.systemUnreadCount); const systemUnreadCount = useMessageStore(state => state.systemUnreadCount);
useEffect(() => {
// 初始化时获取最新未读数
messageManager.fetchUnreadCount();
}, []);
return totalUnreadCount + systemUnreadCount; return totalUnreadCount + systemUnreadCount;
} }

View File

@@ -34,6 +34,24 @@ function buildState(
}; };
} }
let _cachedKey = '';
let _cachedState: ReturnType<typeof buildState>;
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 = { type ThemeState = {
preference: ThemePreference; preference: ThemePreference;
systemScheme: ResolvedScheme; systemScheme: ResolvedScheme;
@@ -58,14 +76,14 @@ export const useThemeStore = create<ThemeState>((set, get) => ({
preference: 'system', preference: 'system',
systemScheme: initialSystemScheme, systemScheme: initialSystemScheme,
hydrated: false, hydrated: false,
...buildState('system', initialSystemScheme), ...buildStateCached('system', initialSystemScheme),
setSystemScheme: (systemScheme) => { setSystemScheme: (systemScheme) => {
const { preference, systemScheme: cur } = get(); const { preference, systemScheme: cur } = get();
if (cur === systemScheme) return; if (cur === systemScheme) return;
set({ set({
systemScheme, systemScheme,
...buildState(preference, systemScheme), ...buildStateCached(preference, systemScheme),
}); });
}, },
@@ -78,7 +96,7 @@ export const useThemeStore = create<ThemeState>((set, get) => ({
const { systemScheme } = get(); const { systemScheme } = get();
set({ set({
preference, preference,
...buildState(preference, systemScheme), ...buildStateCached(preference, systemScheme),
}); });
}, },
@@ -97,7 +115,7 @@ export const useThemeStore = create<ThemeState>((set, get) => ({
preference, preference,
hydrated: true, hydrated: true,
systemScheme, systemScheme,
...buildState(preference, systemScheme), ...buildStateCached(preference, systemScheme),
}); });
}, },
})); }));

View File

@@ -5,6 +5,7 @@
*/ */
import { create } from 'zustand'; import { create } from 'zustand';
import { useShallow } from 'zustand/react/shallow';
import { User, Post, Notification, NotificationBadge } from '../types'; import { User, Post, Notification, NotificationBadge } from '../types';
import { PaginatedData } from '@/services/core'; import { PaginatedData } from '@/services/core';
import { import {
@@ -320,7 +321,7 @@ export const useUserStore = create<UserState>((set, get) => {
// 导出selector hooks以优化性能 // 导出selector hooks以优化性能
export const usePosts = () => useUserStore((state) => state.posts); export const usePosts = () => useUserStore((state) => state.posts);
export const useNotifications = () => useUserStore((state) => state.notifications); 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 useMessageUnreadCount = () => useUserStore((state) => state.messageUnreadCount);
export const useSearchHistory = () => useUserStore((state) => state.searchHistory); export const useSearchHistory = () => useUserStore((state) => state.searchHistory);
export const useIsLoadingPosts = () => useUserStore((state) => state.isLoadingPosts); export const useIsLoadingPosts = () => useUserStore((state) => state.isLoadingPosts);