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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user