perf(profile): improve rendering performance and selector efficiency
Optimize profile-related screens and components by implementing memoization and granular Zustand selectors to reduce unnecessary re-renders. - **Performance Optimization**: - Wrap `TabBar` in `React.memo` to prevent re-renders when parent components update. - Refactor `UserProfileScreen` to use a memoized `ProfileListHeader` component, decoupling the user header from tab state changes. - **State Management**: - Replace object destructuring from `useAuthStore` with granular selectors (e.g., `useAuthStore((s) => s.logout)`) in `AccountDeletionScreen`, `EditProfileScreen`, `FollowListScreen`, and `SettingsScreen` to prevent re-renders on unrelated store changes. - **Code Cleanup**: - Remove redundant case logic in `useUserProfile` hook.
This commit is contained in:
@@ -330,4 +330,4 @@ const TabBar: React.FC<TabBarProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TabBar;
|
export default React.memo(TabBar);
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export const AccountDeletionScreen: React.FC = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { isMobile } = useResponsive();
|
const { isMobile } = useResponsive();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { logout } = useAuthStore();
|
const logout = useAuthStore((s) => s.logout);
|
||||||
const [status, setStatus] = useState<DeletionStatusDTO | null>(null);
|
const [status, setStatus] = useState<DeletionStatusDTO | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|||||||
@@ -279,7 +279,8 @@ export const EditProfileScreen: React.FC = () => {
|
|||||||
const styles = useMemo(() => createEditProfileStyles(colors), [colors]);
|
const styles = useMemo(() => createEditProfileStyles(colors), [colors]);
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { currentUser, updateUser } = useAuthStore();
|
const currentUser = useAuthStore((s) => s.currentUser);
|
||||||
|
const updateUser = useAuthStore((s) => s.updateUser);
|
||||||
const { isWideScreen, isMobile, width } = useResponsive();
|
const { isWideScreen, isMobile, width } = useResponsive();
|
||||||
const responsivePadding = useResponsiveSpacing({ xs: 0, sm: 0, md: 0, lg: 24, xl: 32 });
|
const responsivePadding = useResponsiveSpacing({ xs: 0, sm: 0, md: 0, lg: 24, xl: 32 });
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const FollowListScreen: React.FC = () => {
|
|||||||
const { userId = '', type: typeParam } = useLocalSearchParams<{ userId?: string; type?: string }>();
|
const { userId = '', type: typeParam } = useLocalSearchParams<{ userId?: string; type?: string }>();
|
||||||
const type = typeParam === 'followers' ? 'followers' : 'following';
|
const type = typeParam === 'followers' ? 'followers' : 'following';
|
||||||
|
|
||||||
const { currentUser } = useAuthStore();
|
const currentUser = useAuthStore((s) => s.currentUser);
|
||||||
const { followUser, unfollowUser } = useUserStore.getState();
|
const { followUser, unfollowUser } = useUserStore.getState();
|
||||||
|
|
||||||
// 响应式布局
|
// 响应式布局
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ function createSettingsStyles(colors: AppColors) {
|
|||||||
|
|
||||||
export const SettingsScreen: React.FC = () => {
|
export const SettingsScreen: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { logout } = useAuthStore();
|
const logout = useAuthStore((s) => s.logout);
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { isMobile } = useResponsive();
|
const { isMobile } = useResponsive();
|
||||||
const responsivePadding = useResponsiveSpacing({ xs: 0, sm: 0, md: 0, lg: 24, xl: 32 });
|
const responsivePadding = useResponsiveSpacing({ xs: 0, sm: 0, md: 0, lg: 24, xl: 32 });
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* 支持桌面端双栏布局
|
* 支持桌面端双栏布局
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useMemo, useRef } from 'react';
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
@@ -20,6 +20,67 @@ import { Loading, EmptyState, ResponsiveContainer } from '../../components/commo
|
|||||||
import { useResponsive } from '../../hooks';
|
import { useResponsive } from '../../hooks';
|
||||||
import { useUserProfile, ProfileMode, TABS, TAB_ICONS, createSharedProfileStyles } from './useUserProfile';
|
import { useUserProfile, ProfileMode, TABS, TAB_ICONS, createSharedProfileStyles } from './useUserProfile';
|
||||||
|
|
||||||
|
// 稳定的头部组件:切换 tab 时不会导致 UserProfileHeader 重渲染
|
||||||
|
// 因为 activeTab 只影响 TabBar,UserProfileHeader 的 props 不变
|
||||||
|
interface ProfileListHeaderProps {
|
||||||
|
user: any;
|
||||||
|
isCurrentUser: boolean;
|
||||||
|
isBlocked: boolean;
|
||||||
|
handleFollow: () => void;
|
||||||
|
handleSettings?: () => void;
|
||||||
|
handleEditProfile?: () => void;
|
||||||
|
handleMessage?: () => Promise<void>;
|
||||||
|
handleBlock?: () => Promise<void>;
|
||||||
|
handleFollowingPress: () => void;
|
||||||
|
handleFollowersPress: () => void;
|
||||||
|
activeTab: number;
|
||||||
|
setActiveTab: (tab: number) => void;
|
||||||
|
tabBarContainerStyle: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProfileListHeader = React.memo(function ProfileListHeader({
|
||||||
|
user,
|
||||||
|
isCurrentUser,
|
||||||
|
isBlocked,
|
||||||
|
handleFollow,
|
||||||
|
handleSettings,
|
||||||
|
handleEditProfile,
|
||||||
|
handleMessage,
|
||||||
|
handleBlock,
|
||||||
|
handleFollowingPress,
|
||||||
|
handleFollowersPress,
|
||||||
|
activeTab,
|
||||||
|
setActiveTab,
|
||||||
|
tabBarContainerStyle,
|
||||||
|
}: ProfileListHeaderProps) {
|
||||||
|
if (!user) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<UserProfileHeader
|
||||||
|
user={user}
|
||||||
|
isCurrentUser={isCurrentUser}
|
||||||
|
isBlocked={isBlocked}
|
||||||
|
onFollow={handleFollow}
|
||||||
|
onSettings={handleSettings}
|
||||||
|
onEditProfile={handleEditProfile}
|
||||||
|
onMessage={handleMessage}
|
||||||
|
onBlock={handleBlock}
|
||||||
|
onFollowingPress={handleFollowingPress}
|
||||||
|
onFollowersPress={handleFollowersPress}
|
||||||
|
/>
|
||||||
|
<View style={tabBarContainerStyle}>
|
||||||
|
<TabBar
|
||||||
|
tabs={TABS}
|
||||||
|
activeIndex={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
variant="modern"
|
||||||
|
icons={TAB_ICONS}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
interface UserProfileScreenProps {
|
interface UserProfileScreenProps {
|
||||||
mode: ProfileMode;
|
mode: ProfileMode;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
@@ -110,50 +171,7 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
|
|||||||
);
|
);
|
||||||
}, [loading, activeTab, mode, isBlockedProfile]);
|
}, [loading, activeTab, mode, isBlockedProfile]);
|
||||||
|
|
||||||
// 渲染用户信息头部(与 TabBar 分离,避免切换 tab 时重渲染)
|
// 渲染帖子项/用户不存在状态
|
||||||
const renderUserHeader = useMemo(() => {
|
|
||||||
if (!user) return null;
|
|
||||||
return (
|
|
||||||
<UserProfileHeader
|
|
||||||
user={user}
|
|
||||||
isCurrentUser={isCurrentUser}
|
|
||||||
isBlocked={isBlocked}
|
|
||||||
onFollow={handleFollow}
|
|
||||||
onSettings={handleSettings}
|
|
||||||
onEditProfile={handleEditProfile}
|
|
||||||
onMessage={handleMessage}
|
|
||||||
onBlock={handleBlock}
|
|
||||||
onFollowingPress={handleFollowingPress}
|
|
||||||
onFollowersPress={handleFollowersPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}, [user, isCurrentUser, isBlocked, handleFollow, handleSettings, handleEditProfile, handleMessage, handleBlock, handleFollowingPress, handleFollowersPress]);
|
|
||||||
|
|
||||||
// 渲染 TabBar
|
|
||||||
const renderTabBar = useMemo(() => (
|
|
||||||
<View style={sharedStyles.tabBarContainer}>
|
|
||||||
<TabBar
|
|
||||||
tabs={TABS}
|
|
||||||
activeIndex={activeTab}
|
|
||||||
onTabChange={setActiveTab}
|
|
||||||
variant="modern"
|
|
||||||
icons={TAB_ICONS}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
), [activeTab, setActiveTab, sharedStyles.tabBarContainer]);
|
|
||||||
|
|
||||||
// 渲染 FlashList 头部(用户信息 + TabBar)
|
|
||||||
const renderListHeader = useCallback(() => {
|
|
||||||
if (!user) return null;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{renderUserHeader}
|
|
||||||
{renderTabBar}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}, [user, renderUserHeader, renderTabBar]);
|
|
||||||
|
|
||||||
// 未登录/用户不存在状态
|
|
||||||
if (mode === 'self' && !currentUser) {
|
if (mode === 'self' && !currentUser) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={sharedStyles.container} edges={hasHeader ? ['bottom'] : ['top', 'bottom']}>
|
<SafeAreaView style={sharedStyles.container} edges={hasHeader ? ['bottom'] : ['top', 'bottom']}>
|
||||||
@@ -200,7 +218,21 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{renderListHeader()}
|
<ProfileListHeader
|
||||||
|
user={user}
|
||||||
|
isCurrentUser={isCurrentUser}
|
||||||
|
isBlocked={isBlocked}
|
||||||
|
handleFollow={handleFollow}
|
||||||
|
handleSettings={handleSettings}
|
||||||
|
handleEditProfile={handleEditProfile}
|
||||||
|
handleMessage={handleMessage}
|
||||||
|
handleBlock={handleBlock}
|
||||||
|
handleFollowingPress={handleFollowingPress}
|
||||||
|
handleFollowersPress={handleFollowersPress}
|
||||||
|
activeTab={activeTab}
|
||||||
|
setActiveTab={setActiveTab}
|
||||||
|
tabBarContainerStyle={sharedStyles.tabBarContainer}
|
||||||
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -248,7 +280,23 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
|
|||||||
data={currentPosts}
|
data={currentPosts}
|
||||||
renderItem={renderPostItem}
|
renderItem={renderPostItem}
|
||||||
keyExtractor={postKeyExtractor}
|
keyExtractor={postKeyExtractor}
|
||||||
ListHeaderComponent={renderListHeader}
|
ListHeaderComponent={
|
||||||
|
<ProfileListHeader
|
||||||
|
user={user}
|
||||||
|
isCurrentUser={isCurrentUser}
|
||||||
|
isBlocked={isBlocked}
|
||||||
|
handleFollow={handleFollow}
|
||||||
|
handleSettings={handleSettings}
|
||||||
|
handleEditProfile={handleEditProfile}
|
||||||
|
handleMessage={handleMessage}
|
||||||
|
handleBlock={handleBlock}
|
||||||
|
handleFollowingPress={handleFollowingPress}
|
||||||
|
handleFollowersPress={handleFollowersPress}
|
||||||
|
activeTab={activeTab}
|
||||||
|
setActiveTab={setActiveTab}
|
||||||
|
tabBarContainerStyle={sharedStyles.tabBarContainer}
|
||||||
|
/>
|
||||||
|
}
|
||||||
ListEmptyComponent={renderEmptyPosts}
|
ListEmptyComponent={renderEmptyPosts}
|
||||||
contentContainerStyle={{ paddingBottom: scrollBottomInset }}
|
contentContainerStyle={{ paddingBottom: scrollBottomInset }}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ export const useUserProfile = (options: UseUserProfileOptions): UseUserProfileRe
|
|||||||
handleUserPress(post.author.id);
|
handleUserPress(post.author.id);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'like':
|
case 'like':
|
||||||
case 'unlike':
|
case 'unlike':
|
||||||
post.is_liked ? postSyncService.unlikePost(post.id) : postSyncService.likePost(post.id);
|
post.is_liked ? postSyncService.unlikePost(post.id) : postSyncService.likePost(post.id);
|
||||||
break;
|
break;
|
||||||
@@ -323,13 +323,6 @@ case 'like':
|
|||||||
handlePostPress(post.id, true);
|
handlePostPress(post.id, true);
|
||||||
break;
|
break;
|
||||||
case 'bookmark':
|
case 'bookmark':
|
||||||
case 'unbookmark':
|
|
||||||
post.is_favorited ? postSyncService.unfavoritePost(post.id) : postSyncService.favoritePost(post.id);
|
|
||||||
break;
|
|
||||||
case 'comment':
|
|
||||||
handlePostPress(post.id, true);
|
|
||||||
break;
|
|
||||||
case 'bookmark':
|
|
||||||
case 'unbookmark':
|
case 'unbookmark':
|
||||||
post.is_favorited ? postSyncService.unfavoritePost(post.id) : postSyncService.favoritePost(post.id);
|
post.is_favorited ? postSyncService.unfavoritePost(post.id) : postSyncService.favoritePost(post.id);
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user