♻️ refactor: SidePanel Context to Optimize on ChatView Rerender (#8509)

This commit is contained in:
Danny Avila 2025-07-17 11:31:19 -04:00 committed by GitHub
parent 0b1b0af741
commit dfef7c31d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 180 additions and 139 deletions

View file

@ -0,0 +1,31 @@
import React, { createContext, useContext, useMemo } from 'react';
import type { EModelEndpoint } from 'librechat-data-provider';
import { useChatContext } from './ChatContext';
interface SidePanelContextValue {
endpoint?: EModelEndpoint | null;
}
const SidePanelContext = createContext<SidePanelContextValue | undefined>(undefined);
export function SidePanelProvider({ children }: { children: React.ReactNode }) {
const { conversation } = useChatContext();
/** Context value only created when endpoint changes */
const contextValue = useMemo<SidePanelContextValue>(
() => ({
endpoint: conversation?.endpoint,
}),
[conversation?.endpoint],
);
return <SidePanelContext.Provider value={contextValue}>{children}</SidePanelContext.Provider>;
}
export function useSidePanelContext() {
const context = useContext(SidePanelContext);
if (!context) {
throw new Error('useSidePanelContext must be used within SidePanelProvider');
}
return context;
}

View file

@ -24,4 +24,5 @@ export * from './ToolCallsMapContext';
export * from './SetConvoContext'; export * from './SetConvoContext';
export * from './SearchContext'; export * from './SearchContext';
export * from './BadgeRowContext'; export * from './BadgeRowContext';
export * from './SidePanelContext';
export { default as BadgeRowProvider } from './BadgeRowContext'; export { default as BadgeRowProvider } from './BadgeRowContext';

View file

@ -4,10 +4,10 @@ import { FileSources, LocalStorageKeys } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common'; import type { ExtendedFile } from '~/common';
import { useDeleteFilesMutation } from '~/data-provider'; import { useDeleteFilesMutation } from '~/data-provider';
import DragDropWrapper from '~/components/Chat/Input/Files/DragDropWrapper'; import DragDropWrapper from '~/components/Chat/Input/Files/DragDropWrapper';
import { EditorProvider, SidePanelProvider } from '~/Providers';
import Artifacts from '~/components/Artifacts/Artifacts'; import Artifacts from '~/components/Artifacts/Artifacts';
import { SidePanelGroup } from '~/components/SidePanel'; import { SidePanelGroup } from '~/components/SidePanel';
import { useSetFilesToDelete } from '~/hooks'; import { useSetFilesToDelete } from '~/hooks';
import { EditorProvider } from '~/Providers';
import store from '~/store'; import store from '~/store';
export default function Presentation({ children }: { children: React.ReactNode }) { export default function Presentation({ children }: { children: React.ReactNode }) {
@ -59,6 +59,7 @@ export default function Presentation({ children }: { children: React.ReactNode }
return ( return (
<DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation"> <DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation">
<SidePanelProvider>
<SidePanelGroup <SidePanelGroup
defaultLayout={defaultLayout} defaultLayout={defaultLayout}
fullPanelCollapse={fullCollapse} fullPanelCollapse={fullCollapse}
@ -75,6 +76,7 @@ export default function Presentation({ children }: { children: React.ReactNode }
{children} {children}
</main> </main>
</SidePanelGroup> </SidePanelGroup>
</SidePanelProvider>
</DragDropWrapper> </DragDropWrapper>
); );
} }

View file

@ -7,8 +7,8 @@ import { useMediaQuery, useLocalStorage, useLocalize } from '~/hooks';
import useSideNavLinks from '~/hooks/Nav/useSideNavLinks'; import useSideNavLinks from '~/hooks/Nav/useSideNavLinks';
import { useGetEndpointsQuery } from '~/data-provider'; import { useGetEndpointsQuery } from '~/data-provider';
import NavToggle from '~/components/Nav/NavToggle'; import NavToggle from '~/components/Nav/NavToggle';
import { useSidePanelContext } from '~/Providers';
import { cn, getEndpointField } from '~/utils'; import { cn, getEndpointField } from '~/utils';
import { useChatContext } from '~/Providers';
import Nav from './Nav'; import Nav from './Nav';
const defaultMinSize = 20; const defaultMinSize = 20;
@ -43,13 +43,13 @@ const SidePanel = ({
interfaceConfig: TInterfaceConfig; interfaceConfig: TInterfaceConfig;
}) => { }) => {
const localize = useLocalize(); const localize = useLocalize();
const { endpoint } = useSidePanelContext();
const [isHovering, setIsHovering] = useState(false); const [isHovering, setIsHovering] = useState(false);
const [newUser, setNewUser] = useLocalStorage('newUser', true); const [newUser, setNewUser] = useLocalStorage('newUser', true);
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery(); const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
const isSmallScreen = useMediaQuery('(max-width: 767px)'); const isSmallScreen = useMediaQuery('(max-width: 767px)');
const { conversation } = useChatContext();
const { endpoint } = conversation ?? {};
const { data: keyExpiry = { expiresAt: undefined } } = useUserKeyQuery(endpoint ?? ''); const { data: keyExpiry = { expiresAt: undefined } } = useUserKeyQuery(endpoint ?? '');
const defaultActive = useMemo(() => { const defaultActive = useMemo(() => {

View file

@ -22,14 +22,15 @@ interface SidePanelProps {
const defaultMinSize = 20; const defaultMinSize = 20;
const defaultInterface = getConfigDefaults().interface; const defaultInterface = getConfigDefaults().interface;
const SidePanelGroup = ({ const SidePanelGroup = memo(
({
defaultLayout = [97, 3], defaultLayout = [97, 3],
defaultCollapsed = false, defaultCollapsed = false,
fullPanelCollapse = false, fullPanelCollapse = false,
navCollapsedSize = 3, navCollapsedSize = 3,
artifacts, artifacts,
children, children,
}: SidePanelProps) => { }: SidePanelProps) => {
const { data: startupConfig } = useGetStartupConfig(); const { data: startupConfig } = useGetStartupConfig();
const interfaceConfig = useMemo( const interfaceConfig = useMemo(
() => startupConfig?.interface ?? defaultInterface, () => startupConfig?.interface ?? defaultInterface,
@ -87,6 +88,18 @@ const SidePanelGroup = ({
const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]); const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]);
/** Memoized close button handler to prevent re-creating it */
const handleClosePanel = useCallback(() => {
setIsCollapsed(() => {
localStorage.setItem('fullPanelCollapse', 'true');
setFullCollapse(true);
setCollapsedSize(0);
setMinSize(0);
return false;
});
panelRef.current?.collapse();
}, []);
return ( return (
<> <>
<ResizablePanelGroup <ResizablePanelGroup
@ -135,19 +148,13 @@ const SidePanelGroup = ({
<button <button
aria-label="Close right side panel" aria-label="Close right side panel"
className={`nav-mask ${!isCollapsed ? 'active' : ''}`} className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
onClick={() => { onClick={handleClosePanel}
setIsCollapsed(() => {
localStorage.setItem('fullPanelCollapse', 'true');
setFullCollapse(true);
setCollapsedSize(0);
setMinSize(0);
return false;
});
panelRef.current?.collapse();
}}
/> />
</> </>
); );
}; },
);
export default memo(SidePanelGroup); SidePanelGroup.displayName = 'SidePanelGroup';
export default SidePanelGroup;