refactor(core): optimize state management and component rendering performance
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:
@@ -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 (
|
||||
<View
|
||||
style={{
|
||||
@@ -123,6 +126,7 @@ function SessionGate({ children }: { children: React.ReactNode }) {
|
||||
</View>
|
||||
);
|
||||
}
|
||||
// If verified and not authenticated, the Redirect in (app)/_layout.tsx handles it
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
|
||||
14
src/hooks/useChannels.ts
Normal file
14
src/hooks/useChannels.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
15
src/hooks/useUnreadCount.ts
Normal file
15
src/hooks/useUnreadCount.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export const RegisterScreen: React.FC = () => {
|
||||
resetRegisterData,
|
||||
goToNextStep,
|
||||
goToPrevStep,
|
||||
} = useRegisterStore();
|
||||
} = useRegisterStore.getState();
|
||||
|
||||
// 本地状态
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
@@ -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<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 [showImageViewer, setShowImageViewer] = useState(false);
|
||||
const [postImages, setPostImages] = useState<ImageGridItem[]>([]);
|
||||
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
|
||||
key={`${listKey}_grid`}
|
||||
key="home-grid"
|
||||
ref={scrollViewRef as any}
|
||||
data={displayPosts}
|
||||
extraData={listKey}
|
||||
renderItem={renderGridItem}
|
||||
keyExtractor={keyExtractor}
|
||||
numColumns={gridColumns}
|
||||
@@ -865,9 +865,10 @@ export const HomeScreen: React.FC = () => {
|
||||
// 移动端和宽屏都使用单列 FlashList,宽屏下居中显示
|
||||
return (
|
||||
<FlashList
|
||||
key={listKey}
|
||||
key="home-list"
|
||||
ref={flashListRef}
|
||||
data={displayPosts}
|
||||
extraData={listKey}
|
||||
renderItem={renderPostList}
|
||||
keyExtractor={keyExtractor}
|
||||
contentContainerStyle={{
|
||||
@@ -1062,12 +1063,9 @@ export const HomeScreen: React.FC = () => {
|
||||
{/* 图片查看器 */}
|
||||
<ImageGallery
|
||||
visible={showImageViewer}
|
||||
images={postImages.map(img => ({
|
||||
id: img.id || img.url || String(Math.random()),
|
||||
url: img.url || img.uri || ''
|
||||
}))}
|
||||
images={stableGalleryImages}
|
||||
initialIndex={selectedImageIndex}
|
||||
onClose={() => setShowImageViewer(false)}
|
||||
onClose={stableCloseImageViewer}
|
||||
enableSave
|
||||
/>
|
||||
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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 */}
|
||||
<ImageGallery
|
||||
visible={showImageModal}
|
||||
images={allImages.map(img => ({
|
||||
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 */}
|
||||
<ImageGallery
|
||||
visible={showImageModal}
|
||||
images={allImages.map(img => ({
|
||||
id: img.id || img.url || String(Math.random()),
|
||||
url: img.url || img.uri || ''
|
||||
}))}
|
||||
images={stableGalleryImages}
|
||||
initialIndex={selectedImageIndex}
|
||||
onClose={() => setShowImageModal(false)}
|
||||
enableSave
|
||||
|
||||
@@ -44,7 +44,8 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ 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 {
|
||||
|
||||
@@ -111,6 +111,11 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
|
||||
const [chatImages, setChatImages] = useState<ImageGridItem[]>([]);
|
||||
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<ChatScreenProps> = (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<ChatScreenProps> = (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<ChatScreenProps> = (props) => {
|
||||
{/* 消息列表 */}
|
||||
<View
|
||||
style={styles.messageListContainer}
|
||||
onLayout={e => {
|
||||
scrollPositionRef.current.viewportHeight = e.nativeEvent.layout.height;
|
||||
}}
|
||||
onLayout={stableOnLayout}
|
||||
onTouchEnd={handleDismiss}
|
||||
>
|
||||
{loading ? (
|
||||
@@ -493,16 +518,9 @@ export const ChatScreen: React.FC<ChatScreenProps> = (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<ChatScreenProps> = (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<ChatScreenProps> = (props) => {
|
||||
onInsertEmoji={handleInsertEmoji}
|
||||
onInsertSticker={handleSendSticker}
|
||||
onClose={closePanel}
|
||||
onFocusInput={() => textInputRef.current?.focus()}
|
||||
onFocusInput={stableFocusTextInput}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
@@ -657,10 +670,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
|
||||
{/* 图片查看器 */}
|
||||
<ImageGallery
|
||||
visible={showImageViewer}
|
||||
images={chatImages.map(img => ({
|
||||
id: img.id || img.url || String(Math.random()),
|
||||
url: img.url || img.uri || ''
|
||||
}))}
|
||||
images={stableGalleryImages}
|
||||
initialIndex={selectedImageIndex}
|
||||
onClose={handleCloseImageViewer}
|
||||
enableSave
|
||||
|
||||
@@ -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 = () => {
|
||||
<View style={styles.chatArea}>
|
||||
{showNotifications ? (
|
||||
// 显示系统通知页面
|
||||
<NotificationsScreen onBack={() => setShowNotifications(false)} />
|
||||
<NotificationsScreen onBack={stableCloseNotifications} />
|
||||
) : selectedConversation ? (
|
||||
// 显示选中会话的聊天内容
|
||||
<ChatScreen
|
||||
@@ -849,7 +851,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
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 = () => {
|
||||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||||
{showNotifications ? (
|
||||
// 显示系统通知页面,传入 onBack 回调
|
||||
<NotificationsScreen onBack={() => setShowNotifications(false)} />
|
||||
<NotificationsScreen onBack={stableCloseNotifications} />
|
||||
) : isSearchMode ? (
|
||||
renderSearchMode()
|
||||
) : isWideScreen ? (
|
||||
@@ -882,7 +884,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
renderConversationList()
|
||||
)}
|
||||
{renderActionMenu()}
|
||||
<QRCodeScanner visible={scannerVisible} onClose={() => setScannerVisible(false)} />
|
||||
<QRCodeScanner visible={scannerVisible} onClose={stableCloseScanner} />
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
// 状态
|
||||
|
||||
@@ -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) {
|
||||
{/* ─── 图片预览 ─── */}
|
||||
<ImageGallery
|
||||
visible={showImageModal}
|
||||
images={tradeImages.map(img => ({
|
||||
id: img.id || img.url || String(Math.random()),
|
||||
url: img.url || '',
|
||||
}))}
|
||||
images={stableTradeGalleryImages}
|
||||
initialIndex={selectedImageIndex}
|
||||
onClose={() => setShowImageModal(false)}
|
||||
enableSave
|
||||
|
||||
@@ -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<void>;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set) => ({
|
||||
isAuthenticated: false,
|
||||
currentUser: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
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<AuthState>((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);
|
||||
|
||||
@@ -97,13 +97,14 @@ export function useMessages(
|
||||
loadMoreMessages: (conversationId: string, beforeSeq: number, limit?: number) => Promise<MessageResponse[]>
|
||||
): 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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
preference: ThemePreference;
|
||||
systemScheme: ResolvedScheme;
|
||||
@@ -58,14 +76,14 @@ export const useThemeStore = create<ThemeState>((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<ThemeState>((set, get) => ({
|
||||
const { systemScheme } = get();
|
||||
set({
|
||||
preference,
|
||||
...buildState(preference, systemScheme),
|
||||
...buildStateCached(preference, systemScheme),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -97,7 +115,7 @@ export const useThemeStore = create<ThemeState>((set, get) => ({
|
||||
preference,
|
||||
hydrated: true,
|
||||
systemScheme,
|
||||
...buildState(preference, systemScheme),
|
||||
...buildStateCached(preference, systemScheme),
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -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<UserState>((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);
|
||||
|
||||
Reference in New Issue
Block a user