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
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user