fix(client): reduce unnecessary renders and fetches on hard refresh
Some checks failed
CI / test-python (push) Successful in 4m54s
CI / test-typescript (push) Failing after 57s
CI / test-rust (push) Successful in 1m56s

- 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
This commit is contained in:
2026-01-26 14:59:50 +00:00
parent 01a8d02d60
commit 0d98a36317
4 changed files with 37 additions and 11 deletions

View File

@@ -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<Project[]>([]);
const [activeProjectId, setActiveProjectId] = useState<string | null>(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) => {

View File

@@ -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<Workspace | null>(null);
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
const [currentUser, setCurrentUser] = useState<GetCurrentUserResponse | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(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);

View File

@@ -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;

View File

@@ -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<Set<string>>(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 <Navigate to={`/projects/${activeProject.id}/meetings`} replace />;
}
return (
<div className="p-6 max-w-6xl mx-auto space-y-6">
<div className="flex items-center justify-between">