From 0d98a363175cbc4ff434e91dd8197411239107bc Mon Sep 17 00:00:00 2001 From: Travis Vasceannie Date: Mon, 26 Jan 2026 14:59:50 +0000 Subject: [PATCH] fix(client): reduce unnecessary renders and fetches on hard refresh - Contexts wait for connection before loading to avoid stale cached data - MeetingsPage skips fetch when URL project ID is invalid - Workspace isLoading starts as true for proper loading state - Remove debug logging added during investigation - Add PROJECTS_FETCH_LIMIT constant to timing.ts --- client/src/contexts/project-context.tsx | 12 ++++++++---- client/src/contexts/workspace-context.tsx | 16 ++++++++++++++-- client/src/lib/constants/timing.ts | 3 +++ client/src/pages/Meetings.tsx | 17 ++++++++++++----- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/client/src/contexts/project-context.tsx b/client/src/contexts/project-context.tsx index bcadd7d..462c10a 100644 --- a/client/src/contexts/project-context.tsx +++ b/client/src/contexts/project-context.tsx @@ -5,9 +5,11 @@ import { IdentityDefaults } from '@/api'; import { extractErrorMessage } from '@/api'; import { getAPI } from '@/api/interface'; import type { CreateProjectRequest, Project, UpdateProjectRequest } from '@/api/types'; +import { useConnectionState } from '@/contexts/connection-state'; import { ProjectContext, type ProjectContextValue } from '@/contexts/project-state'; import { projectStorageKey } from '@/contexts/storage'; import { useWorkspace } from '@/contexts/workspace-state'; +import { PROJECTS_FETCH_LIMIT } from '@/lib/constants/timing'; import { errorLog } from '@/lib/observability/debug'; import { readStorageRaw, writeStorageRaw } from '@/lib/storage/utils'; @@ -54,6 +56,7 @@ function fallbackProject(workspaceId: string): Project { export function ProjectProvider({ children }: { children: React.ReactNode }) { const { currentWorkspace } = useWorkspace(); + const { isConnected } = useConnectionState(); const [projects, setProjects] = useState([]); const [activeProjectId, setActiveProjectId] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -75,7 +78,7 @@ export function ProjectProvider({ children }: { children: React.ReactNode }) { const response = await getAPI().listProjects({ workspace_id: workspace.id, include_archived: true, - limit: 200, + limit: PROJECTS_FETCH_LIMIT, offset: 0, }); let preferredId = readStoredProjectId(workspace.id); @@ -110,14 +113,15 @@ export function ProjectProvider({ children }: { children: React.ReactNode }) { } }, []); - // Reload projects when workspace ID changes (not on every workspace object reference change) const workspaceId = currentWorkspace?.id; + const hasLoaded = useRef(false); useEffect(() => { - if (!workspaceId) { + if (!workspaceId || !isConnected || hasLoaded.current) { return; } + hasLoaded.current = true; void loadProjects(); - }, [loadProjects, workspaceId]); + }, [loadProjects, workspaceId, isConnected]); const switchProject = useCallback( (projectId: string) => { diff --git a/client/src/contexts/workspace-context.tsx b/client/src/contexts/workspace-context.tsx index 924c42b..0842216 100644 --- a/client/src/contexts/workspace-context.tsx +++ b/client/src/contexts/workspace-context.tsx @@ -5,13 +5,16 @@ import { IdentityDefaults } from '@/api'; import { getAPI } from '@/api/interface'; import { extractErrorMessage } from '@/api'; import type { GetCurrentUserResponse, Workspace } from '@/api/types'; +import { useConnectionState } from '@/contexts/connection-state'; import { WorkspaceContext, type WorkspaceContextValue } from '@/contexts/workspace-state'; import { workspaceStorageKey } from '@/contexts/storage'; import { readStorageRaw, writeStorageRaw } from '@/lib/storage/utils'; const fallbackUser: GetCurrentUserResponse = { user_id: IdentityDefaults.DEFAULT_USER_ID, + workspace_id: IdentityDefaults.DEFAULT_WORKSPACE_ID, display_name: IdentityDefaults.DEFAULT_USER_NAME, + is_authenticated: false, }; const fallbackWorkspace: Workspace = { id: IdentityDefaults.DEFAULT_WORKSPACE_ID, @@ -44,10 +47,11 @@ function resolveWorkspace(workspaces: Workspace[], preferredId: string | null): } export function WorkspaceProvider({ children }: { children: React.ReactNode }) { + const { isConnected } = useConnectionState(); const [currentWorkspace, setCurrentWorkspace] = useState(null); const [workspaces, setWorkspaces] = useState([]); const [currentUser, setCurrentUser] = useState(null); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const loadContext = useCallback(async () => { @@ -85,9 +89,17 @@ export function WorkspaceProvider({ children }: { children: React.ReactNode }) { } }, []); + const hasLoaded = useRef(false); useEffect(() => { + if (hasLoaded.current) { + return; + } + if (!isConnected) { + return; + } + hasLoaded.current = true; void loadContext(); - }, [loadContext]); + }, [loadContext, isConnected]); // Use ref for workspaces to avoid stale closure in switchWorkspace const workspacesRef = useRef(workspaces); diff --git a/client/src/lib/constants/timing.ts b/client/src/lib/constants/timing.ts index f7c4100..5110dfe 100644 --- a/client/src/lib/constants/timing.ts +++ b/client/src/lib/constants/timing.ts @@ -123,6 +123,9 @@ export const HOME_TASKS_LIMIT = 5; /** Default number of meetings to show on meetings list page */ export const MEETINGS_PAGE_LIMIT = 50; +/** Maximum number of projects to fetch in project context */ +export const PROJECTS_FETCH_LIMIT = 200; + /** Default number of meetings to fetch for tasks page */ export const TASKS_PAGE_MEETINGS_LIMIT = 100; diff --git a/client/src/pages/Meetings.tsx b/client/src/pages/Meetings.tsx index 994adf4..af5ae31 100644 --- a/client/src/pages/Meetings.tsx +++ b/client/src/pages/Meetings.tsx @@ -2,7 +2,7 @@ import { Calendar, Loader2, CheckSquare } from 'lucide-react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import { Navigate, useParams } from 'react-router-dom'; import { getAPI } from '@/api/interface'; import type { Meeting, MeetingState } from '@/api/types'; import type { ProjectScope } from '@/api/types/requests'; @@ -38,7 +38,7 @@ export default function MeetingsPage() { }); const { guard } = useGuardedMutation(); const resolvedProjectId = projectId ?? activeProject?.id; - + const selectedIdsRef = useRef(selectedProjectIds); selectedIdsRef.current = selectedProjectIds; const activeProjects = useMemo( @@ -51,7 +51,6 @@ export default function MeetingsPage() { const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); - // Bulk selection state const [isSelectionMode, setIsSelectionMode] = useState(false); const [selectedMeetingIds, setSelectedMeetingIds] = useState>(new Set()); const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false); @@ -66,9 +65,11 @@ export default function MeetingsPage() { onSuccess: handleDeleteSuccess, }); + const urlProjectValid = !projectId || projects.some((p) => p.id === projectId); const shouldSkipFetch = (projectScope === 'selected' && selectedProjectIds.length === 0) || - (projectScope === 'active' && !resolvedProjectId && !projectsLoading); + (projectScope === 'active' && !resolvedProjectId && !projectsLoading) || + (projectScope === 'active' && projectId && !projectsLoading && !urlProjectValid); const fetchMeetings = useCallback( async (offset: number, append: boolean) => { @@ -86,11 +87,12 @@ export default function MeetingsPage() { } try { + const requestProjectId = projectScope === 'active' ? resolvedProjectId : undefined; const response = await getAPI().listMeetings({ limit: MEETINGS_PAGE_LIMIT, offset, states: stateFilter === 'all' ? undefined : [stateFilter as MeetingState], - project_id: projectScope === 'active' ? resolvedProjectId : undefined, + project_id: requestProjectId, project_ids: projectScope === 'selected' ? selectedIdsRef.current : undefined, include_segments: true, }); @@ -198,6 +200,11 @@ export default function MeetingsPage() { } }; + const projectExistsInList = projectId && projects.some((p) => p.id === projectId); + if (!projectsLoading && projectId && !projectExistsInList && activeProject) { + return ; + } + return (