Project: doctolib-doctolib · Branch: master · Mode: ` · **Model:**claude-haiku-4-5-20251001`
Stats: 1 user messages, 21 tool calls, tools used: Glob, Bash, Read, Grep.
Conversation
Turn 1 — User
Investigate React Query cache behavior for document viewing to understand when and how cache entries are created.
Context
User uploads a document, navigates to patient timeline, and clicks to view the document. The view FAILS with no network request. After browser refresh, viewing works (network request is made).
Evidence shows:
- Before refresh: No GET request when clicking "dokumente anzeigen"
- After refresh: GET request IS made to /doctor_app/documents/{id} and it works
- This suggests React Query is returning cached data before refresh, but executing queryFn after refresh
Investigation Goals
-
Find React Query initialization: - Search for
QueryClientProviderorqueryClientsetup - Check default configuration:refetchOnMount,staleTime,cacheTime- Look in:app/assets/javascripts/contexts/ReactQueryProvider.tsxor similar provider files -
Analyze useDecryptedDocumentsShow hook: - File:
engines/documents/app/assets/javascripts/queries/documents/doctor_app/decryptedDocuments.ts:87-113- Current configuration: What's therefetchOnMountsetting? - What's thestaleTimeandcacheTime? - Are there any other query options that affect refetching? -
Check for prefetching logic: - Search for
usePrefetchDecryptedDocumentsShowusage (line 115 in decryptedDocuments.ts) - Search forprefetchQuerycalls related to documents - Check timeline components that might prefetch document data - Look for calls toqueryClient.setQueryDatathat might populate cache -
Investigate timeline integration: - File:
engines/ehr/app/assets/javascripts/queries/ehr/timelineItems.ts- Does the timeline fetch create cache entries for individual documents? - Are there shared query keys between timeline and document viewer? -
Search for cache manipulation: - Search codebase for
setQueryData.*decrypted_documents- Search for any code that might create cache entries without data - Look for initialization patterns that could create stale cache entries
Key Questions to Answer
-
When does the cache entry get created? - During upload? - When timeline loads? - When hovering over document? - From a previous view session?
-
What's in the cached data? - Is it
{ file: undefined, url: undefined }? - Or some other placeholder value? - Is it marked as 'success' status even with empty data? -
Why doesn't it refetch? - What's the default
refetchOnMountvalue? - Is the cached data within thestaleTimewindow? - Is there a condition preventing the refetch?
Expected Findings
Based on the symptoms, we expect to find:
- refetchOnMount is either false or 'stale' (default React Query behavior)
- staleTime is 5 minutes (already known from line 111)
- Cache entry exists with status='success' but invalid/empty data
- Cache is marked as fresh (not stale) so React Query doesn't refetch
The fix refetchOnMount: true should bypass the staleTime check and always fetch on mount.
Output
Provide:
1. React Query default configuration
2. Current useDecryptedDocumentsShow configuration
3. Any prefetching code found
4. Any cache manipulation code found
5. Explanation of when/how cache entry gets created
6. Confirmation that refetchOnMount: true will solve the issue
Turn 1 — Assistant
I'll investigate the React Query cache behavior for document viewing. Let me start by finding the key files mentioned in your context.
Tools used:
Glob:**/ReactQueryProvider.tsx
Tools used:
Glob:**/decryptedDocuments.ts
Tools used:
Glob:**/timelineItems.ts
Tools used:
Bash:find /Users/USER/workspace/doctolib/doctolib -type f -name "*.ts" -o -name "*.tsx" | grep -i "query" | head -20
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/types/doctor/EventsQuery.ts /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/contexts/ReactQueryProvider.tsx /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/shared/queryParamsHelper.ts /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/components/patient/shared/search/queryInput.test.ts /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/components/patient/shared/search/queryInput.t …(truncated, 1510 more chars)
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/contexts/ReactQueryProvider.tsx
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/decryptedDocuments.ts
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/doctolib/engines/ehr/app/assets/javascripts/queries/ehr/timelineItems.ts
Good, I found the key files. Let me now read them to understand the React Query configuration:
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/contexts/ReactQueryProvider.tsx
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/decryptedDocuments.ts
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib/engines/ehr/app/assets/javascripts/queries/ehr/timelineItems.ts
Tool results:
→ result (ok): 1→import type { ComponentType, ReactNode } from 'react' 2→import { 3→ QueryClientProvider as QueryClientProviderV5, 4→ onlineManager as onlineManagerV5, 5→} from '@tanstack/react-query-v5' 6→import { QueryClientProvider, onlineManager } from '@tanstack/react-query' 7→import lazyLoad from 'components/shared/lazyLoad' 8→import { envIsDevelopment, envIsTest, isOffline } from 'packages/device/environment' 9→// import it from shared_context so that it is a si …(truncated, 2939 more chars)
Tool results:
→ result (ok): 1→import type { AccountContextType } from '@doctolib/shared_context' 2→import { AccountContext } from '@doctolib/shared_context' 3→import type { DecryptFile } from 'engines/e2ee/public' 4→import { TankerContext } from 'engines/e2ee/public' 5→import { useQuery, useQueryClient } from '@tanstack/react-query' 6→import { handleEncryptedDocument } from 'components/shared/documents/utils' 7→import { HttpClientContext } from 'contexts/ReactQueryProvider' 8→import { use …(truncated, 5472 more chars)
Tool results:
→ result (ok): 1→import { useContext, useEffect } from 'react' 2→import { type QueryClient, useIsFetching, useQuery, useQueryClient, keepPreviousData } from '@tanstack/react-query-v5' 3→import { selectTimestamp } from 'services/appointment-list-grouping' 4→import buildTimelineItem from 'components/doctor/desktop/pages/patient/patient-file-tabs/history/items/factory' 5→import { TIMELINE_ITEM_TYPES } from 'components/doctor/desktop/pages/patient/patient-file-tabs/history/items/types' 6→i …(truncated, 26394 more chars)
Excellent! I've found the key configuration. Now let me search for the queryClient initialization to see the default settings:
Tools used:
Grep:queryClient.*QueryClient|new QueryClient|defaultOptions
Tools used:
Glob:**/shared_context*
Tool results:
→ result (ok): <persisted-output> Output too large (75.1KB). Full output saved to: /Users/USER/.claude/projects/-Users-clertonra-workspace-doctolib-doctolib/ec3195ed-8479-410c-9fb2-c5774097f38e/tool-results/toolu_01B5qcaQCUiC4fBNYK9T1emg.txt
Preview (first 2KB): Found 805 files app/assets/javascripts/contexts/doctor/desktop/DocumentImportContext.tsx engines/documents/app/assets/javascripts/queries/documents/doctor_app/decryptedDocuments.ts packages/@doctolib/siilo_ui/src/shared_views/ConversationsListItemMess …(truncated, 1749 more chars)
Tool results:
→ result (ok): No files found
Let me search for the shared_context package where queryClient is initialized:
Tools used:
Bash:find /Users/USER/workspace/doctolib/doctolib -type d -name "shared_context" | head -5
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/doctolib/packages/@doctolib/shared_context
Tools used:
Glob:/Users/USER/workspace/doctolib/doctolib/packages/@doctolib/shared_context/src/**/*.ts
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/doctolib/packages/@doctolib/shared_context/src/InProgressConsultationDocumentsContext.ts /Users/USER/workspace/doctolib/doctolib/packages/@doctolib/shared_context/src/BilleoReadyContext.ts /Users/USER/workspace/doctolib/doctolib/packages/@doctolib/shared_context/src/react-query/queryClientHelpersV5.ts /Users/USER/workspace/doctolib/doctolib/packages/@doctolib/shared_context/src/react-query/queryClientHelpers.ts /Users/USER/workspace/doctolib/doctolib/packages/@doct …(truncated, 1959 more chars)
Perfect! Let me read the query client configuration files:
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib/packages/@doctolib/shared_context/src/react-query/queryClient.ts
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib/packages/@doctolib/shared_context/src/react-query/config.ts
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib/packages/@doctolib/shared_context/src/react-query/queryClientV5.ts
Tool results:
→ result (ok): 1→import type { Query, QueryClientConfig } from '@tanstack/react-query' 2→import { QueryCache, QueryClient } from '@tanstack/react-query' 3→import { captureException } from '@doctolib/error_helpers/public' 4→import { 5→ enhanceErrorMessage, 6→ errorIsHandledByQuery, 7→ httpErrorCanBeIgnored, 8→ isBilleoFetchError, 9→ isHttpError, 10→ isRedirectionError, 11→ shouldNotReportError, 12→} from './queryClientHelpers' 13→import { queryClien …(truncated, 2282 more chars)
Tool results:
→ result (ok): 1→import type { NetworkMode } from '@tanstack/react-query' 2→ 3→export const queryClientDefaultOptions = { 4→ queries: { 5→ refetchOnWindowFocus: false, 6→ retry: false, 7→ networkMode: 'offlineFirst' as NetworkMode, 8→ }, 9→ mutations: { 10→ retry: false, 11→ networkMode: 'offlineFirst' as NetworkMode, 12→ }, 13→} 14→
<system-reminder> Whenever you read a file, you should consider whether it would be considered malw …(truncated, 238 more chars)
Tool results:
→ result (ok): 1→import type { Query, QueryClientConfig } from '@tanstack/react-query-v5' 2→import { MutationCache, QueryCache, QueryClient } from '@tanstack/react-query-v5' 3→import { captureException } from '@doctolib/error_helpers/public' 4→import { 5→ enhanceQueryErrorMessage, 6→ enhanceMutationErrorMessage, 7→ httpErrorCanBeIgnored, 8→ isBilleoFetchError, 9→ isHttpError, 10→ isRedirectionError, 11→ shouldNotReportQueryError, 12→ shouldNotReportMu …(truncated, 2320 more chars)
Now let me search for any prefetching or cache manipulation related to documents:
Tools used:
Bash:grep -r "setQueryData.*decrypted_documents\|prefetchDecryptedDocuments\|setQueryData.*doctor_app.*decrypted" /Users/USER/workspace/doctolib/doctolib --include="*.ts" --include="*.tsx" | head -20
Tool results:
→ result (ok)
Tools used:
Bash:grep -r "usePrefetchDecryptedDocumentsShow" /Users/USER/workspace/doctolib/doctolib --include="*.ts" --include="*.tsx"
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/components/doctor/desktop/pages/documents/Imports.tsx: usePrefetchDecryptedDocumentsShow, /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/components/doctor/desktop/pages/documents/Imports.tsx: const { prefetchDecryptedDocumentShow } = usePrefetchDecryptedDocumentsShow() /Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/decryptedDocuments.ts:export const useP …(truncated, 39 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/components/doctor/desktop/pages/documents/Imports.tsx
Tool results:
→ result (ok): 1→import DroppableZoneButtonModule from 'modules/shared/desktop/DroppableZoneButtonModule/DroppableZoneButtonModule' 2→import { type ChangeEvent, type TouchEvent, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' 3→import { useLocation, useNavigate } from 'react-router-dom' 4→import { omit } from 'lodash-es' 5→import { v4 as uuidv4 } from 'uuid' 6→import type { PDFImage } from 'pdf-lib' 7→import { degrees, PDFDocument } from 'pdf-lib'
…(truncated, 43460 more chars)
Perfect! Now let me search for any other places where cache might be manipulated:
Tools used:
Bash:grep -r "buildDecryptedDocumentQueryKey\|setQueryData.*doctor_app" /Users/USER/workspace/doctolib/doctolib --include="*.ts" --include="*.tsx" 2>/dev/null | head -30
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/decryptedDocuments.ts:export const buildDecryptedDocumentQueryKey = (documentId?: string) => [ /Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/decryptedDocuments.ts: queryKey: buildDecryptedDocumentQueryKey(document?.id), /Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor …(truncated, 1406 more chars)
Let me check the documents.ts file where buildDecryptedDocumentQueryKey is used:
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/documents.ts
Tool results:
→ result (ok): 1→/* 2→ * @file 3→ * @deprecated This file is deprecated and will be removed in future releases. 4→ * Please use the new implementation in packages/@doctolib/documents-ui/src/data/queries/documents.ts. 5→ * This refactor happens because of the react query migration to v5. 6→ * Feel free to move endpoints to this new file. 7→ / 8→ 9→import path from 'path' 10→import { useContext } from 'react' 11→import type { DoctorAppDocument } from '@doctolib/f …(truncated, 25456 more chars)
Excellent! Now let me search for any other components that use useDecryptedDocumentsShow:
Tools used:
Bash:grep -r "useDecryptedDocumentsShow" /Users/USER/workspace/doctolib/doctolib --include="*.ts" --include="*.tsx" 2>/dev/null | grep -v "node_modules"
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/components/doctor/desktop/pages/patient/patient-file-tabs/history/TimelineItemDetails/redesign/DigitalFormItemDetails/MusterItemDetails.tsx:import { useDecryptedDocumentsShow } from 'engines/documents/queries/documents/doctor_app/decryptedDocuments' /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/components/doctor/desktop/pages/patient/patient-file-tabs/history/TimelineItemDetails/redesign/DigitalFormItemDetails/Muster …(truncated, 8722 more chars)
Perfect! Now let me check one of the timeline components that uses this hook to understand the flow better:
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/components/doctor/desktop/pages/patient/patient-file-tabs/history/TimelineItemDetails/hooks/useTimelineDocumentItemController.ts
Tool results:
→ result (ok): 1→import useCarePlansWithQuotation from '@doctolib/financial_os_api/public/hooks/useCarePlansWithQuotation' 2→import { updateDocumentCarePlan } from '@doctolib/financial_os_api/public/modules/carePlanHelper' 3→import type { CarePlanOptionValue } from '@doctolib/financial_os_api/public/types/CarePlanSelectorTypes' 4→import { AccountContext } from '@doctolib/shared_context' 5→import { printDocument } from 'components/doctor/desktop/pages/patient/patient-file-tabs/consultation/U …(truncated, 11201 more chars)
Perfect! Now I have all the information I need. Let me compile the comprehensive investigation report:
React Query Cache Behavior Investigation Report
Based on my thorough analysis of the codebase, I have identified the root cause of the document viewing issue and can confirm the diagnosis and solution.
1. React Query Default Configuration
Location: /Users/USER/workspace/doctolib/doctolib/packages/@doctolib/shared_context/src/react-query/config.ts
export const queryClientDefaultOptions = {
queries: {
refetchOnWindowFocus: false,
retry: false,
networkMode: 'offlineFirst' as NetworkMode,
},
mutations: {
retry: false,
networkMode: 'offlineFirst' as NetworkMode,
},
}
Key finding: The global default for refetchOnMount is NOT explicitly set, which means it defaults to React Query's standard behavior: refetchOnMount: 'stale'
This means:
- React Query will only refetch on mount if data is considered "stale"
- Data is stale if it's older than the staleTime duration
- Data within the staleTime window is considered fresh and will NOT be refetched
2. Current useDecryptedDocumentsShow Configuration
Location: /Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/decryptedDocuments.ts (lines 87-113)
const DOCUMENT_STALE_TIME = 5 * 60 * 1000 // 5 minutes
const DOCUMENT_CACHE_TIME = 5 * 60 * 1000 // 5 minutes
export const useDecryptedDocumentsShow = (
{ document, trackingContext, withToast = true }: DecryptDocumentUrlShowProps,
{ enabled = true, onError }: Options = {}
) => {
// ... setup code ...
return useQuery<DecryptedDocumentsUrlShowType, Error>({
queryKey,
queryFn,
enabled: Boolean(enabled && document?.id),
refetchOnMount: true, // ← ALREADY PRESENT!
onError: onError || ((error) => withToast && toast({ message: error.message, uiStyle: 'warning' })),
cacheTime: DOCUMENT_CACHE_TIME,
staleTime: DOCUMENT_STALE_TIME,
})
}
Critical finding: refetchOnMount: true is ALREADY SET (line 109)!
This means the hook is configured correctly to always refetch when mounting. However, the issue still occurs after upload, suggesting the cache entry is being created with invalid data BEFORE the hook is mounted.
3. Cache Prefetching Logic
Location: /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/components/doctor/desktop/pages/documents/Imports.tsx (lines 237, 336-338)
const { prefetchDecryptedDocumentShow } = usePrefetchDecryptedDocumentsShow()
// ...
const selectDocumentByIndex = useCallback(
(index: number | null) => {
if (typeof index === 'number') {
// ... tracking code ...
const nextDocument = documents[index + 1]
if (nextDocument && 'id' in nextDocument) {
prefetchDecryptedDocumentShow({
document: nextDocument,
})
}
}
setSelectedDocumentIndex(index)
},
[documents, prefetchDecryptedDocumentShow, ...]
)
The prefetch function is used in the Imports page to pre-load the next document. This is normal behavior.
4. Document Upload & Cache Creation Chain
Location: /Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/documents.ts (lines 552-595)
After document creation, the mutation onSuccess handler explicitly refetches the decrypted document query:
onSuccess: async (data, variables) => {
const documentId = data.document.id
// 1. Invalidate the show query for the new document
queryClient.invalidateQueries({ queryKey: buildDocumentsShowQueryKey(documentId) })
// 2. Force refetch of the decrypted document query (PRIMARY FIX)
await queryClient.refetchQueries({
queryKey: buildDecryptedDocumentQueryKey(documentId),
type: 'active', // Only refetch if query is currently mounted
})
// ... rest of invalidations ...
}
KEY ISSUE FOUND: The refetch uses type: 'active' which means:
- It ONLY refetches if the query is currently mounted and being actively observed
- If the component using useDecryptedDocumentsShow is NOT mounted during upload, this refetch is skipped
- The cache entry may not exist, or may contain stale/placeholder data
5. Timeline Integration
Location: /Users/USER/workspace/doctolib/doctolib/engines/ehr/app/assets/javascripts/queries/ehr/timelineItems.ts (lines 490-563)
The timeline doesn't directly create document cache entries. It manages timeline item queries separately. When timeline items are fetched, they contain document data, but this doesn't populate the decrypted_documents cache.
6. When Cache Entry Gets Created - Root Cause Analysis
Scenario: User uploads document → navigates to timeline → clicks "dokumente anzeigen"
The sequence is:
1. Document upload completes → useDocumentsCreate mutation runs
2. Upload onSuccess handler runs → tries to refetch buildDecryptedDocumentQueryKey(documentId) with type: 'active'
3. Problem: The document viewer component (with useDecryptedDocumentsShow hook) is NOT YET MOUNTED, so refetch is skipped
4. Cache state: Query may not have any data, or has placeholder data from failed initialization
5. User navigates to timeline → timeline loads documents
6. User clicks to view document → useDecryptedDocumentsShow hook mounts
7. Hook behavior: Despite refetchOnMount: true, if:
- A cache entry exists with status='success' and data { file: undefined, url: undefined }
- The cache entry is marked fresh (within staleTime)
- React Query may use the cached data instead of refetching
7. Why Browser Refresh Fixes It
After refresh:
- All React Query cache is cleared
- When viewing document, the hook mounts fresh
- useDecryptedDocumentsShow with refetchOnMount: true immediately executes queryFn
- Network request is made and succeeds
8. The Real Problem: Cache Initialization
The issue is likely that when the upload completes:
- The query might be initialized with empty/placeholder data during the upload process
- The
type: 'active'refetch check prevents force-refresh if not mounted - When the query later mounts, the
refetchOnMount: truestill respects thestaleTimecheck in some edge cases
But wait - there's a paradox: refetchOnMount: true should override staleTime!
Let me check the React Query v4 documentation behavior...
Actually, reviewing the code more carefully: refetchOnMount: true in React Query v4 (which this uses based on imports) means ALWAYS refetch on mount, regardless of staleTime.
9. The ACTUAL Root Cause
The real issue is likely in the queryFn itself. Looking at line 67-84 of decryptedDocuments.ts:
queryFn: async () => {
if (!document) return { file: undefined, url: undefined }
if (!document.tanker_encrypted) {
return { file: undefined, url: document.download_path || `/doctor_app/documents/${document.id}` }
}
const { errorMessage, decryptedFileUrl, decryptedFile, resourceId } = await handleEncryptedDocument({
// ...
})
return { file: decryptedFile, url: decryptedFileUrl, resourceId, decryptionError: errorMessage }
}
If the document passed to the hook is incorrect or incomplete (missing tanker_encrypted property), the queryFn returns early with:
{ file: undefined, url: undefined }
This gets cached successfully, and on mount, refetchOnMount: true refetches, but the same incomplete document object is passed again, creating the same invalid result.
10. Why refetchOnMount: true Already Set Doesn't Solve It
The refetchOnMount: true setting is already in the code (line 109), but the issue persists because:
- The document prop might be stale - The component receives an old document object from state/props that …(truncated, 3362 more chars)