fix(app): reduce unnecessary re-renders in sidebar and workspace screens
- Gate WorkspaceScreen on useIsFocused to prevent background rendering - Stabilize SidebarAnimationProvider context with useCallback/useMemo - Wrap LeftSidebar in memo and memoize gesture/styles - Extract inline style objects in AppContainer to stable references
This commit is contained in:
@@ -252,6 +252,9 @@ function QueryProvider({ children }: { children: ReactNode }) {
|
|||||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rowStyle = { flex: 1, flexDirection: "row" } as const;
|
||||||
|
const flexStyle = { flex: 1 } as const;
|
||||||
|
|
||||||
interface AppContainerProps {
|
interface AppContainerProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
selectedAgentId?: string;
|
selectedAgentId?: string;
|
||||||
@@ -377,11 +380,16 @@ function AppContainer({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const containerStyle = useMemo(
|
||||||
|
() => ({ flex: 1 as const, backgroundColor: theme.colors.surface0 }),
|
||||||
|
[theme.colors.surface0]
|
||||||
|
);
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<View style={{ flex: 1, backgroundColor: theme.colors.surface0 }}>
|
<View style={containerStyle}>
|
||||||
<View style={{ flex: 1, flexDirection: "row" }}>
|
<View style={rowStyle}>
|
||||||
{!isMobile && chromeEnabled && <LeftSidebar selectedAgentId={selectedAgentId} />}
|
{!isMobile && chromeEnabled && <LeftSidebar selectedAgentId={selectedAgentId} />}
|
||||||
<View style={{ flex: 1 }}>
|
<View style={flexStyle}>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState, useEffect, useRef, useSyncExternalStore } from 'react'
|
import { memo, useCallback, useMemo, useState, useEffect, useRef, useSyncExternalStore } from 'react'
|
||||||
import { View, Pressable, Text, Platform } from 'react-native'
|
import { View, Pressable, Text, Platform } from 'react-native'
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
@@ -39,7 +39,7 @@ interface LeftSidebarProps {
|
|||||||
selectedAgentId?: string
|
selectedAgentId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarProps) {
|
export const LeftSidebar = memo(function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarProps) {
|
||||||
const { theme } = useUnistyles()
|
const { theme } = useUnistyles()
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
const isMobile = UnistylesRuntime.breakpoint === 'xs' || UnistylesRuntime.breakpoint === 'sm'
|
const isMobile = UnistylesRuntime.breakpoint === 'xs' || UnistylesRuntime.breakpoint === 'sm'
|
||||||
@@ -207,7 +207,9 @@ export function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarPr
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Close gesture (swipe left to close when sidebar is open)
|
// Close gesture (swipe left to close when sidebar is open)
|
||||||
const closeGesture = Gesture.Pan()
|
const closeGesture = useMemo(
|
||||||
|
() =>
|
||||||
|
Gesture.Pan()
|
||||||
.withRef(closeGestureRef)
|
.withRef(closeGestureRef)
|
||||||
.enabled(isOpen)
|
.enabled(isOpen)
|
||||||
// Use manual activation so child views keep touch streams unless we detect
|
// Use manual activation so child views keep touch streams unless we detect
|
||||||
@@ -276,7 +278,32 @@ export function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarPr
|
|||||||
})
|
})
|
||||||
.onFinalize(() => {
|
.onFinalize(() => {
|
||||||
isGesturing.value = false
|
isGesturing.value = false
|
||||||
})
|
}),
|
||||||
|
[
|
||||||
|
isOpen,
|
||||||
|
closeGestureRef,
|
||||||
|
closeTouchStartX,
|
||||||
|
closeTouchStartY,
|
||||||
|
isGesturing,
|
||||||
|
isMobile,
|
||||||
|
windowWidth,
|
||||||
|
translateX,
|
||||||
|
backdropOpacity,
|
||||||
|
animateToClose,
|
||||||
|
animateToOpen,
|
||||||
|
handleClose,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const mobileSidebarInsetStyle = useMemo(
|
||||||
|
() => ({ width: windowWidth, paddingTop: insets.top, paddingBottom: insets.bottom }),
|
||||||
|
[windowWidth, insets.top, insets.bottom]
|
||||||
|
)
|
||||||
|
|
||||||
|
const hostStatusDotStyle = useMemo(
|
||||||
|
() => [styles.hostStatusDot, { backgroundColor: activeHostStatusColor }],
|
||||||
|
[activeHostStatusColor]
|
||||||
|
)
|
||||||
|
|
||||||
const sidebarAnimatedStyle = useAnimatedStyle(() => ({
|
const sidebarAnimatedStyle = useAnimatedStyle(() => ({
|
||||||
transform: [{ translateX: translateX.value }],
|
transform: [{ translateX: translateX.value }],
|
||||||
@@ -303,7 +330,7 @@ export function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarPr
|
|||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
styles.mobileSidebar,
|
styles.mobileSidebar,
|
||||||
{ width: windowWidth, paddingTop: insets.top, paddingBottom: insets.bottom },
|
mobileSidebarInsetStyle,
|
||||||
sidebarAnimatedStyle,
|
sidebarAnimatedStyle,
|
||||||
]}
|
]}
|
||||||
pointerEvents="auto"
|
pointerEvents="auto"
|
||||||
@@ -502,7 +529,7 @@ export function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarPr
|
|||||||
onPress={() => setIsHostPickerOpen(true)}
|
onPress={() => setIsHostPickerOpen(true)}
|
||||||
disabled={hostOptions.length === 0}
|
disabled={hostOptions.length === 0}
|
||||||
>
|
>
|
||||||
<View style={[styles.hostStatusDot, { backgroundColor: activeHostStatusColor }]} />
|
<View style={hostStatusDotStyle} />
|
||||||
<Text style={styles.hostTriggerText} numberOfLines={1}>
|
<Text style={styles.hostTriggerText} numberOfLines={1}>
|
||||||
{activeHostLabel}
|
{activeHostLabel}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -568,7 +595,7 @@ export function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarPr
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
const styles = StyleSheet.create((theme) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
backdrop: {
|
backdrop: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createContext, useContext, useEffect, useRef, type ReactNode } from "react";
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, type ReactNode } from "react";
|
||||||
import { useWindowDimensions } from "react-native";
|
import { useWindowDimensions } from "react-native";
|
||||||
import {
|
import {
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
@@ -78,7 +78,7 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
|
|||||||
}
|
}
|
||||||
}, [isOpen, translateX, backdropOpacity, windowWidth, isGesturing]);
|
}, [isOpen, translateX, backdropOpacity, windowWidth, isGesturing]);
|
||||||
|
|
||||||
const animateToOpen = () => {
|
const animateToOpen = useCallback(() => {
|
||||||
"worklet";
|
"worklet";
|
||||||
translateX.value = withTiming(0, {
|
translateX.value = withTiming(0, {
|
||||||
duration: ANIMATION_DURATION,
|
duration: ANIMATION_DURATION,
|
||||||
@@ -88,9 +88,9 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
|
|||||||
duration: ANIMATION_DURATION,
|
duration: ANIMATION_DURATION,
|
||||||
easing: ANIMATION_EASING,
|
easing: ANIMATION_EASING,
|
||||||
});
|
});
|
||||||
};
|
}, [translateX, backdropOpacity]);
|
||||||
|
|
||||||
const animateToClose = () => {
|
const animateToClose = useCallback(() => {
|
||||||
"worklet";
|
"worklet";
|
||||||
translateX.value = withTiming(-windowWidth, {
|
translateX.value = withTiming(-windowWidth, {
|
||||||
duration: ANIMATION_DURATION,
|
duration: ANIMATION_DURATION,
|
||||||
@@ -100,11 +100,10 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
|
|||||||
duration: ANIMATION_DURATION,
|
duration: ANIMATION_DURATION,
|
||||||
easing: ANIMATION_EASING,
|
easing: ANIMATION_EASING,
|
||||||
});
|
});
|
||||||
};
|
}, [translateX, backdropOpacity, windowWidth]);
|
||||||
|
|
||||||
return (
|
const value = useMemo<SidebarAnimationContextValue>(
|
||||||
<SidebarAnimationContext.Provider
|
() => ({
|
||||||
value={{
|
|
||||||
translateX,
|
translateX,
|
||||||
backdropOpacity,
|
backdropOpacity,
|
||||||
windowWidth,
|
windowWidth,
|
||||||
@@ -112,8 +111,12 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
|
|||||||
animateToClose,
|
animateToClose,
|
||||||
isGesturing,
|
isGesturing,
|
||||||
closeGestureRef,
|
closeGestureRef,
|
||||||
}}
|
}),
|
||||||
>
|
[translateX, backdropOpacity, windowWidth, animateToOpen, animateToClose, isGesturing, closeGestureRef]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarAnimationContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
</SidebarAnimationContext.Provider>
|
</SidebarAnimationContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -519,6 +519,12 @@ export function WorkspaceScreen({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
openIntent,
|
openIntent,
|
||||||
}: WorkspaceScreenProps) {
|
}: WorkspaceScreenProps) {
|
||||||
|
const isFocused = useIsFocused();
|
||||||
|
|
||||||
|
if (!isFocused) {
|
||||||
|
return <View style={{ flex: 1 }} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExplorerSidebarAnimationProvider>
|
<ExplorerSidebarAnimationProvider>
|
||||||
<WorkspaceScreenContent
|
<WorkspaceScreenContent
|
||||||
@@ -539,7 +545,6 @@ function WorkspaceScreenContent({
|
|||||||
const isDarkMode = useColorScheme() === "dark";
|
const isDarkMode = useColorScheme() === "dark";
|
||||||
const mainBackgroundColor = isDarkMode ? theme.colors.surface1 : theme.colors.surface0;
|
const mainBackgroundColor = isDarkMode ? theme.colors.surface1 : theme.colors.surface0;
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const isScreenFocused = useIsFocused();
|
|
||||||
const isMobile =
|
const isMobile =
|
||||||
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||||
|
|
||||||
@@ -1773,7 +1778,7 @@ function WorkspaceScreenContent({
|
|||||||
] as const,
|
] as const,
|
||||||
enabled: Boolean(normalizedServerId && normalizedWorkspaceId),
|
enabled: Boolean(normalizedServerId && normalizedWorkspaceId),
|
||||||
priority: 100,
|
priority: 100,
|
||||||
isActive: () => isScreenFocused,
|
isActive: () => true,
|
||||||
handle: handleWorkspaceTabAction,
|
handle: handleWorkspaceTabAction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1794,7 +1799,7 @@ function WorkspaceScreenContent({
|
|||||||
] as const,
|
] as const,
|
||||||
enabled: Boolean(normalizedServerId && normalizedWorkspaceId),
|
enabled: Boolean(normalizedServerId && normalizedWorkspaceId),
|
||||||
priority: 100,
|
priority: 100,
|
||||||
isActive: () => isScreenFocused,
|
isActive: () => true,
|
||||||
handle: handleWorkspacePaneAction,
|
handle: handleWorkspacePaneAction,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user