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 { extractErrorMessage } from '@/api';
|
||||||
import { getAPI } from '@/api/interface';
|
import { getAPI } from '@/api/interface';
|
||||||
import type { CreateProjectRequest, Project, UpdateProjectRequest } from '@/api/types';
|
import type { CreateProjectRequest, Project, UpdateProjectRequest } from '@/api/types';
|
||||||
|
import { useConnectionState } from '@/contexts/connection-state';
|
||||||
import { ProjectContext, type ProjectContextValue } from '@/contexts/project-state';
|
import { ProjectContext, type ProjectContextValue } from '@/contexts/project-state';
|
||||||
import { projectStorageKey } from '@/contexts/storage';
|
import { projectStorageKey } from '@/contexts/storage';
|
||||||
import { useWorkspace } from '@/contexts/workspace-state';
|
import { useWorkspace } from '@/contexts/workspace-state';
|
||||||
|
import { PROJECTS_FETCH_LIMIT } from '@/lib/constants/timing';
|
||||||
import { errorLog } from '@/lib/observability/debug';
|
import { errorLog } from '@/lib/observability/debug';
|
||||||
import { readStorageRaw, writeStorageRaw } from '@/lib/storage/utils';
|
import { readStorageRaw, writeStorageRaw } from '@/lib/storage/utils';
|
||||||
|
|
||||||
@@ -54,6 +56,7 @@ function fallbackProject(workspaceId: string): Project {
|
|||||||
|
|
||||||
export function ProjectProvider({ children }: { children: React.ReactNode }) {
|
export function ProjectProvider({ children }: { children: React.ReactNode }) {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { isConnected } = useConnectionState();
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
const [activeProjectId, setActiveProjectId] = useState<string | null>(null);
|
const [activeProjectId, setActiveProjectId] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@@ -75,7 +78,7 @@ export function ProjectProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const response = await getAPI().listProjects({
|
const response = await getAPI().listProjects({
|
||||||
workspace_id: workspace.id,
|
workspace_id: workspace.id,
|
||||||
include_archived: true,
|
include_archived: true,
|
||||||
limit: 200,
|
limit: PROJECTS_FETCH_LIMIT,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
});
|
});
|
||||||
let preferredId = readStoredProjectId(workspace.id);
|
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 workspaceId = currentWorkspace?.id;
|
||||||
|
const hasLoaded = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workspaceId) {
|
if (!workspaceId || !isConnected || hasLoaded.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
hasLoaded.current = true;
|
||||||
void loadProjects();
|
void loadProjects();
|
||||||
}, [loadProjects, workspaceId]);
|
}, [loadProjects, workspaceId, isConnected]);
|
||||||
|
|
||||||
const switchProject = useCallback(
|
const switchProject = useCallback(
|
||||||
(projectId: string) => {
|
(projectId: string) => {
|
||||||
|
|||||||
@@ -5,13 +5,16 @@ import { IdentityDefaults } from '@/api';
|
|||||||
import { getAPI } from '@/api/interface';
|
import { getAPI } from '@/api/interface';
|
||||||
import { extractErrorMessage } from '@/api';
|
import { extractErrorMessage } from '@/api';
|
||||||
import type { GetCurrentUserResponse, Workspace } from '@/api/types';
|
import type { GetCurrentUserResponse, Workspace } from '@/api/types';
|
||||||
|
import { useConnectionState } from '@/contexts/connection-state';
|
||||||
import { WorkspaceContext, type WorkspaceContextValue } from '@/contexts/workspace-state';
|
import { WorkspaceContext, type WorkspaceContextValue } from '@/contexts/workspace-state';
|
||||||
import { workspaceStorageKey } from '@/contexts/storage';
|
import { workspaceStorageKey } from '@/contexts/storage';
|
||||||
import { readStorageRaw, writeStorageRaw } from '@/lib/storage/utils';
|
import { readStorageRaw, writeStorageRaw } from '@/lib/storage/utils';
|
||||||
|
|
||||||
const fallbackUser: GetCurrentUserResponse = {
|
const fallbackUser: GetCurrentUserResponse = {
|
||||||
user_id: IdentityDefaults.DEFAULT_USER_ID,
|
user_id: IdentityDefaults.DEFAULT_USER_ID,
|
||||||
|
workspace_id: IdentityDefaults.DEFAULT_WORKSPACE_ID,
|
||||||
display_name: IdentityDefaults.DEFAULT_USER_NAME,
|
display_name: IdentityDefaults.DEFAULT_USER_NAME,
|
||||||
|
is_authenticated: false,
|
||||||
};
|
};
|
||||||
const fallbackWorkspace: Workspace = {
|
const fallbackWorkspace: Workspace = {
|
||||||
id: IdentityDefaults.DEFAULT_WORKSPACE_ID,
|
id: IdentityDefaults.DEFAULT_WORKSPACE_ID,
|
||||||
@@ -44,10 +47,11 @@ function resolveWorkspace(workspaces: Workspace[], preferredId: string | null):
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspaceProvider({ children }: { children: React.ReactNode }) {
|
export function WorkspaceProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const { isConnected } = useConnectionState();
|
||||||
const [currentWorkspace, setCurrentWorkspace] = useState<Workspace | null>(null);
|
const [currentWorkspace, setCurrentWorkspace] = useState<Workspace | null>(null);
|
||||||
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
|
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
|
||||||
const [currentUser, setCurrentUser] = useState<GetCurrentUserResponse | null>(null);
|
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 [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const loadContext = useCallback(async () => {
|
const loadContext = useCallback(async () => {
|
||||||
@@ -85,9 +89,17 @@ export function WorkspaceProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const hasLoaded = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (hasLoaded.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hasLoaded.current = true;
|
||||||
void loadContext();
|
void loadContext();
|
||||||
}, [loadContext]);
|
}, [loadContext, isConnected]);
|
||||||
|
|
||||||
// Use ref for workspaces to avoid stale closure in switchWorkspace
|
// Use ref for workspaces to avoid stale closure in switchWorkspace
|
||||||
const workspacesRef = useRef(workspaces);
|
const workspacesRef = useRef(workspaces);
|
||||||
|
|||||||
@@ -123,6 +123,9 @@ export const HOME_TASKS_LIMIT = 5;
|
|||||||
/** Default number of meetings to show on meetings list page */
|
/** Default number of meetings to show on meetings list page */
|
||||||
export const MEETINGS_PAGE_LIMIT = 50;
|
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 */
|
/** Default number of meetings to fetch for tasks page */
|
||||||
export const TASKS_PAGE_MEETINGS_LIMIT = 100;
|
export const TASKS_PAGE_MEETINGS_LIMIT = 100;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { Calendar, Loader2, CheckSquare } from 'lucide-react';
|
import { Calendar, Loader2, CheckSquare } from 'lucide-react';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from '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 { getAPI } from '@/api/interface';
|
||||||
import type { Meeting, MeetingState } from '@/api/types';
|
import type { Meeting, MeetingState } from '@/api/types';
|
||||||
import type { ProjectScope } from '@/api/types/requests';
|
import type { ProjectScope } from '@/api/types/requests';
|
||||||
@@ -38,7 +38,7 @@ export default function MeetingsPage() {
|
|||||||
});
|
});
|
||||||
const { guard } = useGuardedMutation();
|
const { guard } = useGuardedMutation();
|
||||||
const resolvedProjectId = projectId ?? activeProject?.id;
|
const resolvedProjectId = projectId ?? activeProject?.id;
|
||||||
|
|
||||||
const selectedIdsRef = useRef(selectedProjectIds);
|
const selectedIdsRef = useRef(selectedProjectIds);
|
||||||
selectedIdsRef.current = selectedProjectIds;
|
selectedIdsRef.current = selectedProjectIds;
|
||||||
const activeProjects = useMemo(
|
const activeProjects = useMemo(
|
||||||
@@ -51,7 +51,6 @@ export default function MeetingsPage() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [loadingMore, setLoadingMore] = useState(false);
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
|
|
||||||
// Bulk selection state
|
|
||||||
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
||||||
const [selectedMeetingIds, setSelectedMeetingIds] = useState<Set<string>>(new Set());
|
const [selectedMeetingIds, setSelectedMeetingIds] = useState<Set<string>>(new Set());
|
||||||
const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false);
|
const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false);
|
||||||
@@ -66,9 +65,11 @@ export default function MeetingsPage() {
|
|||||||
onSuccess: handleDeleteSuccess,
|
onSuccess: handleDeleteSuccess,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const urlProjectValid = !projectId || projects.some((p) => p.id === projectId);
|
||||||
const shouldSkipFetch =
|
const shouldSkipFetch =
|
||||||
(projectScope === 'selected' && selectedProjectIds.length === 0) ||
|
(projectScope === 'selected' && selectedProjectIds.length === 0) ||
|
||||||
(projectScope === 'active' && !resolvedProjectId && !projectsLoading);
|
(projectScope === 'active' && !resolvedProjectId && !projectsLoading) ||
|
||||||
|
(projectScope === 'active' && projectId && !projectsLoading && !urlProjectValid);
|
||||||
|
|
||||||
const fetchMeetings = useCallback(
|
const fetchMeetings = useCallback(
|
||||||
async (offset: number, append: boolean) => {
|
async (offset: number, append: boolean) => {
|
||||||
@@ -86,11 +87,12 @@ export default function MeetingsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const requestProjectId = projectScope === 'active' ? resolvedProjectId : undefined;
|
||||||
const response = await getAPI().listMeetings({
|
const response = await getAPI().listMeetings({
|
||||||
limit: MEETINGS_PAGE_LIMIT,
|
limit: MEETINGS_PAGE_LIMIT,
|
||||||
offset,
|
offset,
|
||||||
states: stateFilter === 'all' ? undefined : [stateFilter as MeetingState],
|
states: stateFilter === 'all' ? undefined : [stateFilter as MeetingState],
|
||||||
project_id: projectScope === 'active' ? resolvedProjectId : undefined,
|
project_id: requestProjectId,
|
||||||
project_ids: projectScope === 'selected' ? selectedIdsRef.current : undefined,
|
project_ids: projectScope === 'selected' ? selectedIdsRef.current : undefined,
|
||||||
include_segments: true,
|
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 (
|
return (
|
||||||
<div className="p-6 max-w-6xl mx-auto space-y-6">
|
<div className="p-6 max-w-6xl mx-auto space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
Reference in New Issue
Block a user